2014年4月17日木曜日

mrubyで環境を持つCの関数のprocを生成する

mrubyに、環境を持つCの関数からmrubyのプロシジャを生成するAPIと、取り出す関数が追加された:
mrb_proc_new_cfunc_with_env
mrb_cfunc_env_get

Add API to define cfunc Proc with userdata. · bf6b1df · mruby/mruby

mrubybindではC++のテンプレートを使って、Cの関数を呼び出すためのバインダ関数を自動的に生成することで、mrubyからCの関数を呼び出せるようにしている。バインダ関数は、mrubyから呼び出された引数をC言語の値に変換して、元のCの関数を呼び出し、結果をmrubyの値に変換する。で、このバインダ関数は元のCの関数を知っている必要があるんだけど、上のAPIが追加されるまではmrubyでCの関数が環境を持つことができなかったので、直接は実現できなかった。

そこで以前は、自動的に生成されるバインダの関数の他に、mrubyのdefine_methodでmrubyの関数も生成して、そいつが元のCの関数を保持しておき、バインダ関数に渡すことで実現していた。こうすると実行時にmruby側からCの関数を呼びだそうとした場合には、
  1. define_methodで生成されたmrubyの関数
  2. テンプレートで生成されたバインダ関数
  3. 元のCの関数
と、3段階の関数呼び出しが必要になってしまっていた。

上のAPIを使ってバインダ関数に環境を保持するようにして、
  1. テンプレートで生成されたバインダ関数
  2. 元のCの関数
と、2段階の関数呼び出しに減らすことができた。

またそれにより、mrubybindがバインド時に必要だったmrubyで実装していた補助モジュールが一切必要なくなり、Cから呼び出せるAPIだけで実現できるようになった。

Enjoy!

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++のクラスのデストラクタがちゃんと呼ばれる。


以下は実装の説明。

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()で解放してるけど、解放関数を用意するのであればそうする必要はないのではないだろうか。

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です。



以下は実装の説明。