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;
}