2013年3月19日火曜日

C++用のmrubyの関数バインダを作った

mrubyに任意の型のC言語の関数を登録するためのバインダを作りました。
mrubybind - Binding library for mruby/C++
使い方は簡単で、mrubybind.hをインクルードしてMrubyBindというクラスのインスタンスを生成する。そして、あるC言語の関数foobarがあったとき、
#include "mrubybind.h"
void init(mrb_state* mrb) {
  mrubybind::MrubyBind b(mrb);
  b.bind("foobar", foobar);
}
とすれば、mrubyからその登録した名前で呼び出すことができる。mruby側から渡した引数が自動的にCの関数に渡り、その関数からの戻り値がmruby側に戻る。関数をバインドした後はMrubyBindのオブジェクトは捨ててしまってokです。



以下は実装の説明。

mrubyに登録できる関数は、mrb_func_t
typedef mrb_value (*mrb_func_t)(mrb_state *mrb, mrb_value);
という、決まった型である必要がある。Cに存在する既存のライブラリをmrubyから呼び出して使いたい場合、ラッパー関数を作って、引数をmrubyの値(mrb_value)からC言語で扱える型に変換し、戻り値を逆に変換してmrubyに返してやる必要がある。しかしこれを関数1個1個に対しすべて手書きで用意するのは面倒だ。そこで自動的にラッパー関数を作ってくれるバインダを作ってみた。

仕組みはC++テンプレートの特殊化を使って、渡されたCの関数の引数の型や数、戻り値の型(または戻り値なし)によってバインドする関数を自動的に生成する。Binderという空のテンプレートクラスを用意して
template <class T>
struct Binder {
};
それがある型だった場合の特殊化した定義を書くことで処理を分けることができる。例えば引数にintを受け取ってconst char*を返す関数のための特殊化したテンプレート
template<>
struct Binder<int (*)(int)> {
  static mrb_value call(mrb_state* mrb, void* p, mrb_value* args, int narg) {
    const char* (*fp)(int) = (const char* (*)(int))p;  // 関数ポインタ
    const char* result = fp(mrb_fixnum(args[0]));  // 関数呼び出し
    return mrb_str_new_cstr(mrb, result);
  }
};
など、必要な関数の型に合うバインダをすべて用意してやれば Binder<Cの関数の型>::call で呼び出すことができる。

でもまだこれだと、引数がintの場合、真偽値の場合、文字列の場合、…などの場合分けが必要になってしまうので、引数や戻り値の型に対してもテンプレートを使う。Cとmrubyの型の変換を行うテンプレートクラスのベース
template <class T>
struct Type {
};
を用意して、mrb_value型との変換を行うget()ret()というクラス関数を用意する:
// Fixnum
template<>
struct Type<int> {
  static int get(mrb_value v) { return mrb_fixnum(v); }
  static mrb_value ret(mrb_state*, int i) { return mrb_fixnum_value(i); }
};

// String
template<>
struct Type<const char*> {
  static const char* get(mrb_value v) { return RSTRING_PTR(v); }
  static mrb_value ret(mrb_state* mrb, const char* s) { return mrb_str_new_cstr(mrb, s); }
};

//... 変換したい型に応じて用意する
これを使って、
template<class R, class P0>
struct Binder<R (*)(P0)> {
  static mrb_value call(mrb_state* mrb, void* p, mrb_value* args, int narg) {
    R (*fp)(P0) = (R (*)(P0))p;  // 関数ポインタ
    R result = fp(Type<P0>::get(args[0]));  // 関数呼び出し
    return Type<R>::ret(mrb, result);
  }
};
などとすれば、型変換が用意されている型を使っている、引数が1つで戻り値ありの関数をすべてバインドできるようになる。同様に引数が2つ、3つ、…と必要な引数の数分用意すればテンプレートでマッチングしてくれる。

引数の数や戻り値のあり/なしに関してもうまくまとめる方法がないので、それぞれ用意する必要がある。

Squirrel用のバインダsqbindでは、登録するCの関数の型からバインダの関数をテンプレートで生成して、その生成した関数をSquirrelに登録するんだけど、その際にクロージャを使って呼び出したいCの関数を保持している。この場合SquirrelのスクリプトからCの関数の呼び出しは間にバインダの関数(Cクロージャ)が入るだけですむ。それに対してこちらのmrubybindは、mrubyでCの関数を登録する際に追加のデータを保持する方法がわからなかったので(名前的にmrb_closure_new_cfunc()がそれっぽいんだけど、使い方わからず)いったんRubyの関数側でクロージャを作って、それがバインダ関数を呼び出して、ようやくそれが目的の関数を呼び出す。なので余分なオーバーヘッドが入ってしまっている。


あとはC++のクラスをmruby側にバインドできるようにしたい。→追加した

0 件のコメント :

コメントを投稿