カスタムアロケータと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が全部置き換わっちゃうので、それはそれで問題かも。うーむ。