9
10

More than 5 years have passed since last update.

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

Last updated at Posted at 2013-12-04

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;
}
9
10
4

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
10