マルチプラットフォームで動作する、Rubyで書かれたTwitterクライアントmikutterのプラグイン、mikutter-perlを書きました。
動機: RubyからPerlを呼びたい
前々からNamazuのRuby実装を作りたい、ということを考えていました。現状のNamazuは独自のデータ形式を持っていますが、新しい実装ではバックエンドをGroongaにしたいと思っています。現状、Groongaのperl bindingはこれといった定番がないのに対し、RubyにはRroongaがあるので、作るならRubyだろうと考えています。
ただ、これまでPerlで書かれたフィルター群をどうにか継承できないか、とも考えていました。Namazuのフィルターはさまざまな形式のファイルからテキストを抽出するもので、今となっては結構な種類の数があります。
フィルター部分のみを呼び出すために、RubyからPerlのコードを呼び出したいという考えを実現する最初の一歩として、mikutter-perlを書いてみたというわけです。なお、Perl側には逆のことをするInline::Rubyというものがあります。
libperl呼び出し部分
最初に書いておきますが、やっていることは大したことありません。perlembedのサンプルとそう大きくは変わりません。
当初は直接libperl.soをDL::Importで読み込んで実行することを考えていたのですが、その手法には問題がありました。構造体へのアクセスができず、初期化に必要な作業が実行できません。以下にperlembedでも出てくるコード例を示します。
#include <EXTERN.h> #include <perl.h> int main(int argc, char **argv, char **env) { PERL_SYS_INIT3(&argc,&argv,&env); my_perl = perl_alloc(); perl_construct(my_perl); PL_exit_flags |= PERL_EXIT_DESTRUCT_END; perl_parse(my_perl, NULL, argc, argv, (char **)NULL); perl_run(my_perl); perl_destruct(my_perl); perl_free(my_perl); PERL_SYS_TERM(); }
肝となるのはPL_exit_flagsです。perl_alloc, perl_constructで作成したperlインタプリタのインスタンス的なものに対し、終了時の挙動をフラグとして設定する箇所です。
gcc -Eオプションを使ってこの部分がどのような内容に展開されるかを確認すると、以下のようになります(on Debian wheezy)。
$ gcc -I /usr/lib/perl/5.14/CORE perltest.c -E (中略) static PerlInterpreter *my_perl; int main(int argc, char **argv, char **env) { Perl_sys_init3(&argc, &argv, &env); my_perl = perl_alloc(); perl_construct(my_perl); (my_perl->Iexit_flags) |= 0x02; perl_parse(my_perl, ((void *)0), argc, argv, (char **)((void *)0)); perl_run(my_perl); perl_destruct(my_perl); perl_free(my_perl); Perl_sys_term(); }
Perlinterpreterはstruct interpreterをtypedefしたもので、my_perlのIexit_flagsに値を代入するようなコードに展開されます。
デバッグ情報、シンボル情報などをstripした状態で、dlopen(2)だけでこれらにアクセスすることはできません。そもそもPerl周りはたくさんのマクロを使っているので、そこをクリアしたとしても汎用性が担保できません。
結局、そのあたりを全部担当するためのコードをCで書き、シンプルなインターフェースだけにまとめてDL::Import(dlopen)で呼び出せるようなコードを書きました(perlstub.c)。
RubyとPerlの間のやりとりも単純な文字列だけなので、割とシンプルに書きあがりました(mikutter-perl.c)。DL::Importはchar *を自動的にRubyの文字列に変換してくれるので楽です。
PL_exit_flagsさえ気にしなければ、DL::Importだけでコードを完結させることはできます。実際に書いたコードをgistにおいてあります。
mikutterプラグインの開発にあたって
今回初めてmikutterのプラグインを作成しました。参考にした資料は以下の通りです。
- Writing mikutter plugin (http://toshia.github.io/writing-mikutter-plugin/)
- 基本的な書き方とイベントフィルタ、アクティビティの扱いについての情報
- Event and Filters (http://toshia.github.io/writing-mikutter-plugin/event.html)
- 利用可能なイベントの一覧。今回は自分の実際のポストのみを対象とするためpostedイベントを監視
- mikutter-shell-post (https://github.com/penguin2716/mikutter_shell_post)
- 似たようなことを実現している先行例として大いに参考になりました
mikutter pluigin特有だと思われる、自分がはまった点についても列挙しておきます。
- プラグイン内部で発生した例外は自力で受けないとスルーされる。ちょっとしたエラーがあっても止まらないのでわからない
- mypostイベントは、mikutter起動時に取得される過去の自身のツイートを受け取った時にも発生する。今まさにポストしたツイートに対応するイベントはpostedになる