Rubyの拡張ライブラリを作るときはメモリ確保失敗時に自分でGCしてリトライしなきゃダメという話
はじめに
表題のとおりでよく考えれば当たり前なのですが、ちょっとハマったので書いておきます。
要約すると、Rubyの拡張ライブラリを作る場合、用意されているALLOC()などのメモリ確保マクロを使わないときには、ちゃんと自分でGCを呼ぶ必要がある、という話です。
本題
こんなライブラリがあったとして
// foo_lib.hpp #include <stdlib.h> typedef struct Foo { double bar[1000000]; } Foo; Foo* create_foo() { Foo* foo = (Foo*)malloc(sizeof(Foo)); if (foo == NULL) throw "<Memory allocation failed>"; return foo; } void release_foo(Foo* foo) { free(foo); }
こんな感じでRubyの拡張ライブラリをつくるとします。
// foo.cpp #include "ruby.h" #include "foo_lib.hpp" // Fooクラスのインスタンスを割り当てる VALUE foo_alloc(VALUE klass) { Foo *ptr; try { ptr = create_foo(); } catch (const char* msg) { std::cerr << msg << std::endl; exit(-1); } return Data_Wrap_Struct(klass, 0, release_foo, ptr); } extern "C" { void Init_foo() { VALUE foo; foo = rb_define_class("Foo", rb_cObject); rb_define_alloc_func(foo, foo_alloc); } }
で、こんな感じで使おうとすると...
require './foo.so' loop { foo = Foo.new puts 'loop' }
$ ./foo.rb
loop
loop
...
loop
途中でメモリ確保に失敗してエラーになってしまいます。
Fooオブジェクトのインスタンスを割り当てるときにData_Wrap_Struct()を使っているので、参照されなくなったらGCで回収されるはずなのに…。
なんでかというと
メモリが足りなくなってもGCが呼ばれないから。
ruby.hで宣言されているALLOC()などのメモリ確保マクロでは、メモリ確保失敗時にGCを行ってからやり直すようになっています。
しかし、Rubyとまったく関係ないライブラリのメモリ確保関数を使った場合は、当然ですがRubyのGCは呼ばれません。
そこで、さっきのソースコード(foo.cpp)のfoo_alloc()を
VALUE foo_alloc(VALUE klass) { Foo *ptr; try { ptr = create_foo(); } catch (...) { // ダメだったらGCしてリトライ rb_gc_start(); try { ptr = create_foo(); } catch (...) { // それでもダメならあきらめる exit(-1); } } return Data_Wrap_Struct(klass, 0, release_foo, ptr); }
のようにしてやることで、メモリが確保できなかったときにはちゃんとGCが動くようになります。
まとめ
RubyではALLOC()やALLOC_N()などの便利なメモリ確保マクロが用意されてるので、なるべくそっちを使いましょう。
ライブラリのラッパーを作ってるときなど、ALLOC()が使えないときには、ちゃんと自分でGCを呼ぶ必要があります。