カスタムアロケータとC++のnew
mruby Advent Calendar 2013 - Qiitaの19日目の記事です。
mrubyでカスタムアロケータを使う
mrubyを使うときは、まずmrb_open()を呼ぶわけですが、mrb_open_allocf()という独自のアロケータを指定できる亜種があります。
mrb_state* mrb_open_allocf(mrb_allocf, void *ud);
mrb_allocfは次のようにtypedefされています
typedef void* (*mrb_allocf) (struct mrb_state *mrb, void*, size_t, void *ud);
mrb_allocf()の引数ですが、2番目のvoid *はreallocする場合の元の領域へのポインタ(新しい領域ならNULL), size_tには確保したいサイズ(バイト数。0の場合は解放しろという意味)が渡されます。void *udはユーザデータでmrb_open_allocf()に渡したものがそのまま渡されて、自由に使えるようです。
では、何も考えずにmrb_open()を呼び出した場合はどうなっているのでしょうか?
mrb_open()は内部ではmrb_open_allocf()を読んでいるだけで、その際に最初の引数として内部で定義されているallocf()を渡しています。で、allocf()は次のようになっています。
static void* allocf(mrb_state *mrb, void *p, size_t size, void *ud) { if (size == 0) { free(p); return NULL; } else { return realloc(p, size); } }
ふむふむ。素直にrealloc()とfree()を使っています。
で、まぁmrb_allocf()を自分で定義してmrb_open_allocf()に指定してやると独自のアロケータが使えるわけです。mruby内のあらゆるメモリ確保は独自のアロケータ経由になりますし、mrb_malloc()などのメモリ確保関数も内部的には独自のアロケータを呼んでくれます。
メモリの少ないシステムや、診断などをしたい場合に使えそうですね。
mrbgemでのメモリ確保
mrubyを使う側がカスタムアロケータを使う可能性があるので、いろんな人に使ってもらうmrbgemを書く際はメモリ確保にmalloc()を直接呼ぶのではなく、mrb_malloc()を呼ぶのが基本のようです。
mrbgemでC++のnewは?
んじゃmrbgemでC++のnew使うときはどうすんのよという話がでてきますが、placement new(配置new)を使えば良さそうに思えます。
C++のクラスFooをラップしたRubyのクラスFooをmrbgemで定義する例を書いてみます。あ、ちょうど2日前に構造体をラップする方法をtsaharaさんが解説されているので、mruby で C 言語の構造体をラップしたオブジェクトを作る正しい方法 - Qiitaを参考にします。
#include <new> #include "mruby/class.h" #include "mruby/value.h" #include "mruby/data.h" class Foo; //どこかで定義されていることにする void foo_free(mrb_state *mrb, void *ptr){ Foo *foo = (Foo *)ptr; foo->~Foo(); //デストラクタを明示的に呼び出し mrb_free(mrb, foo); //メモリ解放 } //Fooのタイプ定義 const static struct mrb_data_type mrb_foo_type = { "Foo", foo_free }; mrb_value foo_init(mrb_state *mrb, mrb_value self){ void *p = mrb_malloc(mrb, sizeof(Foo)); Foo *newFoo = new(p) Foo(); //placement newを使ってFoo::Foo()を呼ぶ DATA_PTR(self) = newFoo; DATA_TYPE(self) = &mrb_foo_type; return self; } void mrb_mruby_foo_gem_init(mrb_state *mrb) { struct RClass *fooClass; fooClass = mrb_define_class(mrb, "Foo", mrb->object_class); MRB_SET_INSTANCE_TT(fooClass, MRB_TT_DATA); mrb_define_method(mrb, fooClass, "initialize", foo_init, MRB_ARGS_NONE()); } void mrb_mruby_foo_gem_final(mrb_state *mrb) { }
placement newで本当に大丈夫?
Fooが単純なクラスの場合はこれで良さそうです。実際mruby-arduinoではArduinoのServoクラスをこの方法でラップしています。
でもFooの実装の中で普通のnewを使っている場合には対応できませんね。。複雑なライブラリでnewしまくっている場合やSTLまで使われている場合はどうすりゃいいんでしょう?
そのような場合はoperator newをオーバーロードしてやれば良さそうです。ただ、そうすると今度はmrubyを含むプログラムのnewが全部置き換わっちゃうので、それはそれで問題かも。うーむ。