SWIG でスクリプト言語用バインディングを書いた

先週末から SWIG を使って分類器と学習器の Perl/Python/Ruby/Lua バインディングを書いていた.3年ぐらい前に書いた分類器の Ruby バインディングは,Ruby の C インタフェースが変わってそのままでは動かなかったので,結局一から書き直すことに.Python -> Ruby -> Perl -> Lua と書いてきて一番大変だったのは Perl.学部の時少し使っただけで,もはや文法をほとんど覚えていなかったので,何よりテスト用のスクリプトを書くのが大変だった.
SWIG で書いた std::vector 用の入力インタフェースは以下.スクリプト言語側の配列を C++ の std::vector に詰め直して関数に渡すだけ.色々拾ったり編集したりしたものだけど,言語横断的に見れたほうが便が良いのでメモしておく.swig-2.0.3 + Perl 5.8.9/5.10.1/5.12.3 / Python 2.6.6/2.7.1 / Ruby 1.8.7/1.9.2 / Lua 5.1.4 / LuaJIT (git 先端) で動いている.
まず 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

RubyPython とほぼ同じ.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++ のデータ型をスクリプト言語側で直接使えるようにする方が良いのだけど,そうするとスクリプト言語側のライブラリが使えなくなったりするので悩ましい.
その他,気づいた点をメモしておく.

SWIG の話とは直接関係ないが,親切な方が分類器の Portfile を MacPorts に登録してくれたようだ.というわけで,MacPorts 経由で初めて PATH が通ったディレクトリ (~/ports/bin) に自分の分類器をインストールしてみた.コマンドライン一行でインストールできるのは便利だなー.
今後はファイル名にバージョンをつけておいた方が良さそうだ.