読者です 読者をやめる 読者になる 読者になる

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を呼ぶ必要があります。