2013年3月30日土曜日

mrubybindにC++のクラスバインダを追加した

Cの任意の関数をmrubyにバインドできるmrubybindに、C++のクラスやメンバ関数をバインドする機能を追加した。

mrubybind - github

使い方は、コンストラクタ用のヘルパー関数を用意してbind_class()でクラスをバインド、bind_class_method()でメソッドをバインドできる。
Foo* new_foo(int x) {
  return new Foo(x);
}

void install_foo_class(mrb_state* mrb) {
  mrubybind::MrubyBind b(mrb);
  b.bind_class("Foo", new_foo);
  b.bind_class_method("Foo", "bar", &Foo::bar);
}

するとmrubyから呼び出せるようになる:
foo = Foo.new(123)
p foo.bar(567)
ガベージコレクト時にはC++のクラスのデストラクタがちゃんと呼ばれる。


以下は実装の説明。

昨日mrubyにC++のクラスを持ち込む方法がわかったので、それを関数バインダを作ったときのようにテンプレートを使ってある程度自動化する。

mrubyのDATA型は型情報mrb_data_typeとして名前と解放関数を必要とする。それをClassBinderというテンプレートクラスを定義して、deleteを呼び出す関数dtorを型ごとに用意してやる:
template <class C>
struct ClassBinder {
  static struct mrb_data_type type_info;
  static void dtor(mrb_state*, void* p) {
    C* instance = static_cast<C*>(p);
    delete instance;
  }
};
template<class C>
mrb_data_type ClassBinder<C>::type_info = { "???", dtor };
テンプレートクラスにも静的メンバ変数を定義できて、それを解放関数で初期化する(テンプレートで文字列を与える方法がわからなかったので、ダミーとして一律に同じ文字列"???"を与えているが、これでいいんだろうか?)。

任意の型のメンバ関数をテンプレートで受け付けるには、普通の関数の登録時とだいたい同じ具合で
template<class C, class P0>
struct ClassBinder<void (C::*)(P0)> {
  ...
などとすればマッチングされる。

相違点として、関数ポインタをmrubyに渡す際にはmrubyのVOIDP型として保持しているが、C++のメンバ関数のポインタは関数ポインタと同じ幅とは限らない(ex. 8バイトに対して16バイト)のでその手段は使えない。なので今回はメンバ関数ポインタをそのサイズのバイト列(文字列)を確保して保持することにした:
  template <class Method>
  void bind_class_method(const char* class_name, const char* method_name, Method m) {
    ...
    mrb_value mp = mrb_str_new(mrb, (char*)&m, sizeof(m));
    ...
使うときにはRSTRING_PTR()で文字列のポインタを取り出して、その中身をメンバ関数ポインタとして扱う。

0 件のコメント :

コメントを投稿