2013年3月29日金曜日

mrubyにC++のクラスを持ち込む

C++側で定義されているクラスをmrubyに持ち込む方法を調べた。参考にしたのはmruby-time/time.cmruby/C構造体組み込みを読む - Code Reading Wiki。正しいかどうかは保証できないけれど、一応ちゃんと動いているようです。

C++側になにかクラス
class Hoge {
public:
  Hoge(int x) : x_(x) {
    std::cout << "Hoge::ctor()" << std::endl;
  }
  virtual ~Hoge() {
    std::cout << "Hoge::dtor()" << std::endl;
  }
  int x() const { return x_; }

private:
  int x_;
};
があったとして、これをmruby側から扱いたい場合には、DATA型を使うといいようだ:
#include <mruby.h>
#include <mruby/class.h>
#include <mruby/data.h>

static void hoge_free(mrb_state *mrb, void *ptr) {
  Hoge* hoge = static_cast<Hoge*>(ptr);
  delete hoge;
}

static struct mrb_data_type hoge_type = { "Hoge", hoge_free };

static mrb_value hoge_initialize(mrb_state *mrb, mrb_value self) {
  // Initialize data type first, otherwise segmentation fault occurs.
  DATA_TYPE(self) = &hoge_type;
  DATA_PTR(self) = NULL;

  mrb_int x;
  mrb_get_args(mrb, "i", &x);
  Hoge* hoge = new Hoge(x);

  DATA_PTR(self) = hoge;
  return self;
}

static mrb_value hoge_to_s(mrb_state *mrb, mrb_value self) {
  Hoge* hoge = static_cast<Hoge*>(mrb_get_datatype(mrb, self, &hoge_type));
  char buf[32];
  snprintf(buf, sizeof(buf), "#Hoge<%d>", hoge->x());
  return mrb_str_new_cstr(mrb, buf);
}

void install_hoge_class(mrb_state* mrb) {
  struct RClass *tc = mrb_define_class(mrb, "Hoge", mrb->object_class);
  MRB_SET_INSTANCE_TT(tc, MRB_TT_DATA);
  mrb_define_method(mrb, tc, "initialize", hoge_initialize, ARGS_REQ(1));
  mrb_define_method(mrb, tc, "inspect", hoge_to_s, ARGS_NONE());
  mrb_define_method(mrb, tc, "to_s", hoge_to_s, ARGS_NONE());
}
DATA型用の型情報hoge_typeを用意する、それには解放時に呼ばれる関数を登録できる。あとはmrb_define_class()でmrubyのクラスを定義する。そしてMRB_SET_INSTANCE_TTMRB_TT_DATAにする(これはHogeクラスのインスタンスの型はDATA型だということを設定しているのだろうか?)。コンストラクタinitializeでC++のクラスのインスタンスを生成し、DATA_PTR(self)にセットしてやる。また型情報hoge_typeDATA_TYPE(self)にセットしてやる(これをしないとSegmentation faultが発生する)。
あとはC++のメンバ関数に対してmrb_define_method()でバインドしてやればmruby側からメソッドとして呼び出すことができる。
ここでは試しに、inspectto_sを定義している。データポインタの取り出しはmrb_get_datatype()でできて、型が違った場合はNULLが返る(上のコードではチェックしてない)。

ここまですれば、あとはmruby側から呼び出せる:
#include <mruby.h>
#include <mruby/compile.h>

int main() {
  mrb_state* mrb = mrb_open();
  install_hoge_class(mrb);
  std::cout << "Start" << std::endl;
  mrb_load_string(mrb,
                  "hoge = Hoge.new(123)\n"
                  "p hoge\n"
                  "hoge = nil\n"
                  );
  std::cout << "End" << std::endl;
  mrb_close(mrb);
  return 0;
}
// 実行結果:
// Start
// Hoge::ctor()
// #Hoge<123>
// End
// Hoge::dtor()
ガベコレされるときにちゃんと解放関数が呼び出される。

  • mruby-timeではDATA型を使う以外に、Time.nowなどのクラスメソッドではData_Wrap_Struct()マクロを使ってラップオブジェクトを作ってるみたいなんだけど、よくわからず…
  • mruby-timeでは、Cの構造体mrb_timeもmrb_malloc()でメモリ確保、mrb_free()で解放してるけど、解放関数を用意するのであればそうする必要はないのではないだろうか。

2 件のコメント :

  1. hoge_initialize関数についてですが、mrb_get_argsの前でDATA_TYPE, DATA_PTRを設定しておかないと、ArgumentErrorが投げられたときに初期化されないままインスタンスが生成されてしまい、GCで落ちるようです。

    mruby-time/time.cでもmrb_get_argsの前に初期化しています。

    返信削除
  2. たしかに、Hoge.new()への引数の数や型を間違えるとエラーが出て、その後segmentation faultで落ちますね。参考になりました。ソースを修正しました。

    返信削除