mysqlにperlインタプリタを組み込んでみる
以前「mysqlに独自関数を組み込む(UDF)」というエントリを書いてみましたが、今回はそれの延長で、mysqlのUDFとしてperlインタプリタを組み込んでみようと言う実験です。
mysqlにperlを組み込んで何がうれしいかっていうと、簡単なperlスクリプトで自由にmysqlの関数を作れるようになります。
例えば
- URLリストから実際にLWPで内容を取得してきて何かの処理をした結果を表示したり
- perlの正規表現で任意のフィルタリング関数を書いてみたり
- 何かの更新処理のあとに裏でバッチ処理を走らせるためのトリガースクリプトを関数として組み込んでみたり
みたいなことがmysqlの中だけでできるようになります。
車輪の再開発
実は先に述べておきますと、今回のこのアイデアですが、なんと今から5年ほど前にかの有名なBrian Aker氏がLinuxJournalにてEmbedding Perl in MySQLという記事を世に出していました。その名もMyPerl。
そうとは知らず「これはおもしろいアイデアを思いついたぞ!」と思って一人で地味に実験を繰り返してきたのですが、お正月休みの大半を使って「ほぼ完成」という段階にまでなって、Aker氏のMyPerlを知りました。まさに車輪の再開発!!というかMyPerlの存在を知らなかったことがとても残念!
とはいえ、せっかくなのでMyPerlと自分の書いた実験コードを見比べたところ、基本的な流れや構成は驚くほど一緒でした。ふむふむ、実験としてはまぁまぁ良い線いってたんだな。
というわけで、一応自信が持てたので自分の書いたなんちゃってコードを晒しておきます。perlの内部構造やEmbedを理解したい、という人にちょうどよいレベルの内容になっています。
簡単な流れ
perlはExtutils::Embedというモジュールを使ってCにインタプリタを組み込むことができます。
この仕組みを使って
・perlインタプリタを生成
・任意のスクリプトを記述したファイルをパース
・実行
と動いてくれるようなCのコードを作ります。
そしてこれをMySQLのUDFのAPIにあうカタチの関数に包んであげます。それでもって共有オブジェクト(so)としてコンパイル。
あとは出来上がったsoファイルを適切な場所においてmysql上で関数登録します。
最後に適当なperlスクリプトを書いてみて、それをmysqlからユーザ定義関数として呼び出せればできあがりです。
※関数名は「p」とします。作るsoファイルはp.soとします。
ソース
全部で100行にも満たないコードなんでまずは一気に晒します。
#include <EXTERN.h> #include <perl.h> #include <mysql.h> #include <dlfcn.h> EXTERN_C void xs_init (pTHX); static PerlInterpreter *my_perl; char *path = "/tmp/"; char *buff; void PerlStack (char *result, int argc, char *argv[]) { dSP; int i; ENTER; SAVETMPS; PUSHMARK (sp); for (i = 1; i < argc; i++) { XPUSHs (sv_2mortal (newSVpv (argv[i], 0))); } PUTBACK; perl_call_pv ("pfunc", G_SCALAR); SPAGAIN; sprintf (result, "%s", POPp); PUTBACK; FREETMPS; LEAVE; } my_bool p_init (UDF_INIT * initid, UDF_ARGS * args, char *message) { void *h; char *embedding[] = { "perl", "/tmp/default.pl", "", "" }; buff = (char *) malloc (sizeof (char) * (strlen (path) + 1)); strcpy (buff, path); if (args->arg_count > 0) { buff = (char *) realloc (buff, sizeof (char) * (strlen (buff) + strlen (args->args[0]) + 1)); strcat (buff, args->args[0]); embedding[1] = buff; } my_perl = perl_alloc (); perl_construct (my_perl); h = dlopen ("/usr/local/lib/perl5/5.8.8/i686-linux-thread-multi/CORE/libperl.so", RTLD_GLOBAL | RTLD_LAZY); perl_parse (my_perl, xs_init, 4, embedding, environ); perl_run (my_perl); dlclose (h); return 0; } void p_deinit (UDF_INIT * initid __attribute__ ((unused))) { perl_destruct (my_perl); perl_free (my_perl); free (buff); } char * p (UDF_INIT * initid __attribute__ ((unused)), UDF_ARGS * args, char *result, unsigned long *length, char *is_null, char *error __attribute__ ((unused))) { PerlStack (result, args->arg_count, args->args); *length = strlen (result); return result; }
以下、コードの上から順番に要点だけ解説します。
冒頭部分
#include <EXTERN.h> #include <perl.h> #include <mysql.h> #include <dlfcn.h> EXTERN_C void xs_init (pTHX); static PerlInterpreter *my_perl; char *path = "/tmp/"; char *buff;
冒頭部分ですがEXTERN.hとperl.hは組み込みperlを作るときは必ず必要になります。
mysql.hはmysqlのUDFとして必要。
あとdlfcn.hは、あとで説明します。xs_initの部分も後で説明します。
static PerlInterpreter *my_perl;としている部分でperlインタプリタを生成する部分のメモリ宣言しておきます。
これはmy_perlというのはインタプリタそのものでコード全体を通じて登場してきます。
ちなみにこの実験コードでは多くのPerl Embedコードのサーンプルに習ってstaticで宣言してますが、Aker氏のMyPerlではmyperl_passableという構造体を作ってそこのメンバとしてハンドリングしていました。
pathとbuffというのは、ちょっとイケてないんですが、mysqlクライアントからスクリプトファイルを指定する際のパス文字列の入れ物です。置き場所を/tmpの下としてハードコードしています。ここらへん、だいぶかっこ悪いですが、実験ということでご容赦を。
スタックの操作
void PerlStack (char *result, int argc, char *argv[]) { dSP; int i; ENTER; SAVETMPS; PUSHMARK (sp); for (i = 1; i < argc; i++) { XPUSHs (sv_2mortal (newSVpv (argv[i], 0))); } PUTBACK; perl_call_pv ("pfunc", G_SCALAR); SPAGAIN; sprintf (result, "%s", POPp); PUTBACK; FREETMPS; LEAVE; }
ここがこの実験コードのキモです。なにをしてるかというとperlインタプリタに対して引数を渡して結果を取得しています。
まず普通は目にしないコードなのでまるで呪文のようですが、ほとんで全部マクロです。perlの内部では関数の呼び出しの際のデータの授受をこのようなスタック操作マクロで実装してるんですね。
ここら辺を詳しくしりたい方はperlembedやperlcall、perlapiなどのドキュメントを読んでみてください。
このコードでは引数は何個でも好きな数だけ指定できて、戻り値は1つだけ、というようにしています。引数/戻り値ともにSV(Scalar Value)なイメージにしてみました。
初期化(スクリプトのファイルパスを受け取ってperlインタプリタを作るまで)
my_bool p_init (UDF_INIT * initid, UDF_ARGS * args, char *message) { void *h; char *embedding[] = { "perl", "/tmp/default.pl", "", "" }; buff = (char *) malloc (sizeof (char) * (strlen (path) + 1)); strcpy (buff, path); if (args->arg_count > 0) { buff = (char *) realloc (buff, sizeof (char) * (strlen (buff) + strlen (args->args[0]) + 1)); strcat (buff, args->args[0]); embedding[1] = buff; } my_perl = perl_alloc (); perl_construct (my_perl); h = dlopen ("/usr/local/lib/perl5/5.8.8/i686-linux-thread-multi/CORE/libperl.so", RTLD_GLOBAL | RTLD_LAZY); perl_parse (my_perl, xs_init, 4, embedding, environ); perl_run (my_perl); dlclose (h); return 0; }
ここの前半部分はだいぶ変だと思います。mysqlクライアントから渡されたスクリプトのパス文字列を最終的にembedding[1]に入れたいんですが、わざわざpathとbuffという2つの変数を用意しておいてmallocしたりreallocしたり。。要するにファイルパスの文字列を格納したいだけです。もっとスマートに書く方法はあるのかと思いますが、Cは経験不足なんでうまい書き方がわかりませんでした。
後半部分はインタプリタ用のメモリを確保(perl_alloc)して、実際にインタプリタを作って(perl_construct)、パース(perl_parse)して、スタンバイOK な状態(perl_run)にまで持っていっています。この流れはperl embedのスタンダードな手順です。
しかしここで1点だけ注意が必要です。
perl_parseの第2引数にxs_initというのを指定していますが、これはこのperl embedから呼び出されるperlスクリプトがさらにCのライブラリを使うような場合(要するにXS)、必要となります。
つまり C→perl→XS と呼び出すためのXSへのグルーになります。なので例えばSocketを使うようなモジュールを使う場合にはxs_initを指定しなければなりません。
この部分の解説はperlembedにも詳しく書いてあるんですが、しかし残念ながらそれだけではどうしてもmysqlからの呼び出しではDynaloader(XSLoader)近辺でundefined symbol等のエラーが出てしまいました。
相当色々と試行錯誤して、何日も悩んでようやくそれを回避する方法がわかったのですが、それがdlopenの部分です。libperl.soをRTLD_GLOBALとして明示的にopenしておくことでmysqlからの呼び出しでもうまくXSコードが動くようになりました。くわしい仕組みはよくわかりませんが、兎も角、これで今は動くようになってます。libperl.soのパスをハードコードしているあたり、だいぶイケてないですが、これも実験コードということでご容赦を。
後始末の部分
void
p_deinit (UDF_INIT * initid __attribute__ ((unused)))
{
perl_destruct (my_perl);
perl_free (my_perl);
free (buff);
}
ここは説明いらないですね。みたまんまです。
実行部分
char * p (UDF_INIT * initid __attribute__ ((unused)), UDF_ARGS * args, char *result, unsigned long *length, char *is_null, char *error __attribute__ ((unused))) { PerlStack (result, args->arg_count, args->args); *length = strlen (result); return result; }
この部分はSQLで処理対象となるレコードの数だけ繰り返し実行されます。
実質的にはPerlStackという関数を呼び出すだけです。これは前述したスタック操作の関数です。
そしてresultをかえしています。
コンパイル
perl -MExtUtils::Embed -e xsinit -- -o perlxsi.c gcc -shared -Wall -O -I/usr/local/mysql/include/mysql `perl -MExtUtils::Embed -e perl_inc` `perl -MExtUtils::Embed -e ccopts` p.c perlxsi.c `perl -MExtUtils::Embed -e ldopts` -o p.so
まずExtUtils::Embedでperlxsi.cを自動生成します。これはxs_initを指定しるために必要となるコードです。
で続いてコンパイルです。p.cとperlxsi.cからp.soを作っています。コンパイルオプションの大半もExtUtils::Embedが面倒を見てくれます。自分で明示的に指定するのはmysql.hの場所くらいです。
デプロイ&関数登録
出来上がったp.soをLD_LIBRARY_PATHの通ったどこかにおいて上げます。/usr/lib/ とかでも良いし、自分で管理しやすい場所におけばOKです。
関数の登録はmysqlコマンドで実行します。関数名はとにかく短くしたかったのでperlの"p"にしてみました。
CREATE FUNCTION p RETURNS STRING SONAME 'p.so';
perlスクリプトを用意
どんなものでもかまいませんが、拙作のHTML::Featureを使って「与えられたURLからそのページのタイトルだけを持ってくる」関数にしてみます。
「そんなのmysqlの関数として本当に必要なのか?」という声が聞こえてきそうですが、まぁ実験なんで(笑)。ちょうど前述のXSを使う場合(C→perl→XS)の検証にもなるので、これでやってみることにします。
use HTML::Feature; sub pfunc{ my $url = shift; return HTML::Feature->new()->parse($url)->title(); }
これを/tmp/test.plとして保存しておきます。
実際に叩いてみる
mysql> select p("test.pl","http://d.hatena.ne.jp/download_takeshi/"); +---------------------------------------------------------+ | p("test.pl","http://d.hatena.ne.jp/download_takeshi/") | +---------------------------------------------------------+ | ダウンロードたけし(寅年)の日記 | +---------------------------------------------------------+ 1 row in set (0.98 sec)
あ、できた。