Ruby
C
mruby
mrubyDay 4

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

More than 3 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;
}