SWIG でスクリプト言語用バインディングを書いた
先週末から SWIG を使って分類器と学習器の Perl/Python/Ruby/Lua バインディングを書いていた.3年ぐらい前に書いた分類器の Ruby バインディングは,Ruby の C インタフェースが変わってそのままでは動かなかったので,結局一から書き直すことに.Python -> Ruby -> Perl -> Lua と書いてきて一番大変だったのは Perl.学部の時少し使っただけで,もはや文法をほとんど覚えていなかったので,何よりテスト用のスクリプトを書くのが大変だった.
SWIG で書いた std::vector
まず Python.型チェックを細かく入れておく.
#ifdef SWIGPYTHON %typemap (in) (std::vector <unsigned int> &) (std::vector <unsigned int> vec) { if (! PyList_Check ($input)) { PyErr_SetString (PyExc_TypeError, "not a list"); return NULL; } int len = PyList_Size ($input); vec.reserve (len); for (int i = 0; i < len; ++i) { PyObject *po = PyList_GetItem ($input, i); if (! PyInt_Check (po)) { PyErr_SetString (PyExc_TypeError, "not a int"); return NULL; } vec.push_back (PyInt_AsLong (po)); } $1 = &vec; } #endif
Ruby は Python とほぼ同じ.1.9 以降は配列の長さを取るのに RARRAY_LEN を使う必要がある.
#ifdef SWIGRUBY %typemap (in) (std::vector <unsigned int> &) (std::vector <unsigned int> vec) { Check_Type ($input, T_ARRAY); int len = RARRAY_LEN ($input); vec.reserve (len); for (int i = 0; i < len; ++i) { VALUE ro = rb_ary_entry ($input, i); Check_Type (ro, T_FIXNUM); vec.push_back (FIX2UINT (ro)); } $1 = &vec; } #endif
Perl は参照外し (SvRV) が必要なのと,av_len が配列の最大の添字を返す(実際の要素数-1)のに注意が必要.
#ifdef SWIGPERL %typemap (in) (std::vector <unsigned int> &) (std::vector <unsigned int> vec) { if (! SvROK ($input) or SvTYPE (SvRV ($input)) != SVt_PVAV) croak ("not a list"); int len = av_len (reinterpret_cast <AV *> (SvRV ($input))) + 1; vec.reserve (len); for (int i = 0; i < len; ++i) { SV **po = av_fetch (reinterpret_cast <AV *> (SvRV ($input)), i, 0); if (! SvUOK (*po)) croak ("not an integer"); vec.push_back (SvUV (*po)); } $1 = &vec; } #endif
最後に Lua のインタフェース.Lua の仮想スタック L を操作する必要があったり,配列 (table) の長さが取れなかったりするため Perl/Python/Ruby とはだいぶ違う感じになる.swig/Lib/lua/std_vector.i に似たようなものがあるけど,lua_isnil の if 節が間違っている気がする.
#ifdef SWIGLUA %typemap (in) (std::vector <unsigned int> &) (std::vector <unsigned int> vec) { if (! lua_istable (L, $input)) return 0; int i = 0; while (1) { lua_rawgeti (L, $input, i+1); if (lua_isnil (L, -1)) { lua_pop (L, 1); std::fprintf (stderr, "not a list\n"); break; } if (! lua_isnumber (L, -1)) { lua_pop (L, 1); std::fprintf (stderr, "not an integer\n"); return 0; } vec.push_back (static_cast <unsigned int> (lua_tonumber (L, -1))); lua_pop (L, 1); ++i; } $1 = &vec; } #endif
パフォーマンスを考えると,C/C++ のデータ型をスクリプト言語側で直接使えるようにする方が良いのだけど,そうするとスクリプト言語側のライブラリが使えなくなったりするので悩ましい.
その他,気づいた点をメモしておく.
- スクリプト言語側で C インタフェースが変更される場合も多いので,SWIG はなるべく最新版を使う.
- 変数のスコープや動的なメモリ管理がどうなっているかを確認するには,SWIG が出力した *_wrap.cxx を見るのが手っ取り早い.valgrind だとエラーが出て大変だったりする(Python だと valgrind-python.supp, Ruby では patch があるようだけど)
- C/C++ コンパイラはスクリプト言語をインストールしたときと同じバージョンのものを使う(MacPorts で別の版の gcc を入れている場合など注意; gcc_select で APPLE gcc に戻す)Python は setup.py がよきに計らってくれるが,Perl/Ruby では Makefile.PL/extconf.rb が出力する Makefile が gcc のフラグの加減でコンパイルできなかったりした (-arch とか).
- ruby 1.9 では C++ に渡した文字列が壊れる場合があったので,std::string でコピーを保持するように拡張ライブラリ側を変更した.
- 関数呼び出しの速度は,スクリプト言語ごとに少し差があるようだ.軽い関数を呼び出すときには気になるかも知れない.(slower) Python >> Perl > Ruby >= Lua >>> C++ (faster) 引数変換も含んでこんな感じ(いい加減).興味がある人はこの辺りもどうぞ: ruby - Why is equivalent Python code so much slower - Stack Overflow, SWIG を使ったときの性能(メモ) - やた@はてな日記
- 例外とかどうするのやら(exception.i を読みこめば良いのか).C++ ライブラリに処理が渡っている間は Ctrl-C が効かない(Python/Ruby).不便.
SWIG の話とは直接関係ないが,親切な方が分類器の Portfile を MacPorts に登録してくれたようだ.というわけで,MacPorts 経由で初めて PATH が通ったディレクトリ (~/ports/bin) に自分の分類器をインストールしてみた.コマンドライン一行でインストールできるのは便利だなー.
今後はファイル名にバージョンをつけておいた方が良さそうだ.