Edited at
mrubyDay 4

GUIアプリケーションなどが保持するmrubyのオブジェクトのGC対策

More than 5 years have passed since last update.


GUIアプリケーションなどが保持するmrubyのオブジェクトのGC対策

Code on Rmakeにてmrubyをゲームを記述する言語として組み込んでみました。

そのとき以下のような場面によく遭遇しました。

マウス入力を受け取るProcオブジェクトpをC++側の変数に保存する関数を呼ぶ


ほかのmrubyの処理をおこなう。
(このときgcが発生してpのメモリーが解放されてしまう)

マウス入力をおこなう

pを呼ぼうとするがメモリーが解放されているためアプリケーションが落ちる

これだと困るので、pをgcされないようにはどうしたらいいかなと考えました。

Code on RmakeではC++からmrubyのメソッドの呼び出しを超えて

生存することが期待されるobject(上の例だとp)は、定数Hashを用意して

そこに入れることでgcの対象にならないようにしました。

以下でコードの解説をしますが、

cのサンプルコードは長くなってしまったので記事の最後に配置しました。

#

my_print "root begin"

H = {}

def init
my_print "init begin"
set_proc do
my_print "call back proc"
end
my_print "init end"
end

def gc_trigger
my_print "gc_trigger begin"
GC.start
my_print "gc_trigger end"
end

my_print "root end"

実行結果は以下のようになります。

root begin

root end
init begin
init end
gc_trigger begin
gc_trigger end
call back proc

仮にcのプログラムの以下の部分をコメントアウトすると

    // if don't set proc to 'H', proc will delete by gc.

mrb_hash_set(mrb, h, mrb_symbol_value(mrb_intern(mrb, "proc")), proc);

"call back proc"と出力されるところまでは実行されず、

アプリケーションが落ちてしまいます。

単にmrb_value型の変数に代入しておいただけでは、

その変数が解放されなくても、それが参照しているオブジェクトは解放されうります。

処理の内容をより詳しく見てみます。

最初にmrubyのinitメソッドを呼びproc変数に関数オブジェクトへの参照を代入します。

    // call set_proc in init.

{
call_method(mrb, mrb_obj_value(mrb->top_self), mrb_intern(mrb, "init"));
}

def init

my_print "init begin"
set_proc do
my_print "call back proc"
end
my_print "init end"
end

// set callback function

mrb_value set_proc(mrb_state *mrb, mrb_value self)
{
mrb_get_args(mrb, "&", &proc);

mrb_value h = mrb_const_get(mrb, mrb_obj_value(mrb->object_class), mrb_intern(mrb, "H"));

// if don't set proc to 'H', proc will delete by gc.
mrb_hash_set(mrb, h, mrb_symbol_value(mrb_intern(mrb, "proc")), proc);

return self;
}

ここで定数HにあるHashオブジェクトにprocを加えておかないと後で

アプリケーションが落ちます。

ProcオブジェクトだけでなくHash, Array, 文字列, classのインスタンスなど参照型のもので

呼び出した関数より外で使う場合は、すべてHに加えなければなりません。

数値、シンボルなど値型の場合は加える必要はないです。

Hに対してオブジェクトを加える処理は

mrubyで記述してもいいかもしれません。

次にgcを呼ぶような処理をおこないます。

    // do gc.

{
call_method(mrb, mrb_obj_value(mrb->top_self), mrb_intern(mrb, "gc_trigger"));
}

def gc_trigger

my_print "gc_trigger begin"
GC.start
my_print "gc_trigger end"
end

ここでは確実にgcが起こるようにしていますが、

一般的な処理では必ずしもgcは起こらないため

アプリケーションが落ちたり落ちなかったりということもあり、

一見すると原因がわかりにくいこともありました。

もちろんgcが発生したにもかかわらず、同じアドレスに別のメモリーが確保されて

access violationは起こらないが、

なんだかんだで処理がおかしくなるということもありました。

最初に加えたprocをここで呼び出します。

// call proc.

{
call_method(mrb, proc, mrb_intern(mrb, "call"));
}

gcされてしまっていると多くの場合ここでaccess violationが起こり

アプリケーションが落ちてしまいます。

以上、GUIアプリケーションなどmrubyを扱う側で、mrubyのオブジェクトを保持する際の

gc付近の引っかかりやすいポイントについて書いてみました。

GUIアプリケーションでなくてもmrubyの外側でmrubyのオブジェクトを持つ場合には

同じようなことに遭遇しうるかと思います。

(サンプルはGUIアプリケーションではないですし。)

ご参考になれば幸いです。

最後にサンプルのコードを以下に置きます。

#include "mruby.h"

#include "mruby/proc.h"
#include "mruby/string.h"
#include "mruby/variable.h"
#include "mruby/value.h"
#include "mruby/class.h"
#include "mruby/hash.h"
#include "mruby/compile.h"
#include <stdio.h>
#include <stdlib.h>

char* read_text_file(const char* file_name)
{
FILE* f = fopen(file_name, "rt");
if (!f)
{
return NULL;
}
char* buf;
int len;
fseek(f, 0, SEEK_END);
len = ftell(f);
fseek(f, 0, SEEK_SET);
buf = (char*)malloc(len + 1);
fread(buf, len, 1, f);
buf[len] = '\0';
return buf;
}

void error_check(mrb_state* mrb)
{
mrb_value obj;
if(mrb->exc)
{
obj = mrb_obj_value(mrb->exc);
obj = mrb_funcall(mrb, obj, "inspect", 0);
printf("error: %s\n", RSTRING_PTR(obj));
}
}

mrb_value proc;

// set callback function
mrb_value set_proc(mrb_state *mrb, mrb_value self)
{
mrb_get_args(mrb, "&", &proc);

mrb_value h = mrb_const_get(mrb, mrb_obj_value(mrb->object_class), mrb_intern(mrb, "H"));

// if don't set proc to 'H', proc will delete by gc.
mrb_hash_set(mrb, h, mrb_symbol_value(mrb_intern(mrb, "proc")), proc);

return self;
}

// output for debug
mrb_value my_print(mrb_state *mrb, mrb_value self)
{
mrb_value s;
mrb_get_args(mrb, "S", &s);

printf("%s\n", mrb_string_value_ptr(mrb, s));

return self;
}

// call object's method.
void call_method(mrb_state* mrb, mrb_value object, mrb_sym method){
int ai;
ai = mrb_gc_arena_save(mrb);
mrb_value args[] = {};
mrb_funcall_argv(mrb, object, method, 0, args);
error_check(mrb);
mrb_gc_arena_restore(mrb, ai);
}

int main()
{
mrb_state* mrb;
mrb_parser_state* mrbst;

char* source = read_text_file("avoid_method_gc_sample.rb");

mrb = mrb_open();
mrbst = mrb_parse_string(mrb, source, NULL);
free(source);

RProc* n;
n = mrb_generate_code(mrb, mrbst);
mrb_pool_close(mrbst->pool);

{
mrb_define_method(mrb, mrb->object_class, "set_proc", set_proc, ARGS_REQ(1));
mrb_define_method(mrb, mrb->object_class, "my_print", my_print, ARGS_REQ(1));
}

{
int ai;
mrb_value v;
ai = mrb_gc_arena_save(mrb);
v = mrb_run(mrb, n, mrb_nil_value());
error_check(mrb);
mrb_gc_arena_restore(mrb, ai);
}

// call set_proc in init.
{
call_method(mrb, mrb_obj_value(mrb->top_self), mrb_intern(mrb, "init"));
}

// do gc.
{
call_method(mrb, mrb_obj_value(mrb->top_self), mrb_intern(mrb, "gc_trigger"));
}

// call proc.
{
call_method(mrb, proc, mrb_intern(mrb, "call"));
}

mrb_close(mrb);

return 0;
}