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


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

[Code on Rmake](https://core.rmake.jp/ "Code on Rmake")にてmrubyをゲームを記述する言語として組み込んでみました。
そのとき以下のような場面によく遭遇しました。

```
マウス入力を受け取るProcオブジェクトpをC++側の変数に保存する関数を呼ぶ
↓
ほかのmrubyの処理をおこなう。
（このときgcが発生してpのメモリーが解放されてしまう）
↓
マウス入力をおこなう
↓
pを呼ぼうとするがメモリーが解放されているためアプリケーションが落ちる
```

これだと困るので、pをgcされないようにはどうしたらいいかなと考えました。
[Code on Rmake](https://core.rmake.jp/ "Code on Rmake")ではC++からmrubyのメソッドの呼び出しを超えて
生存することが期待されるobject(上の例だとp)は、定数Hashを用意して
そこに入れることでgcの対象にならないようにしました。
以下でコードの解説をしますが、
cのサンプルコードは長くなってしまったので記事の最後に配置しました。

```ruby
#

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のプログラムの以下の部分をコメントアウトすると

```cpp
    // 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変数に関数オブジェクトへの参照を代入します。

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

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

```cpp
// 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を呼ぶような処理をおこないます。

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

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

ここでは確実にgcが起こるようにしていますが、
一般的な処理では必ずしもgcは起こらないため
アプリケーションが落ちたり落ちなかったりということもあり、
一見すると原因がわかりにくいこともありました。
もちろんgcが発生したにもかかわらず、同じアドレスに別のメモリーが確保されて
access violationは起こらないが、
なんだかんだで処理がおかしくなるということもありました。

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

```cpp
// call proc.
    {
        call_method(mrb, proc, mrb_intern(mrb, "call"));
    }
```
gcされてしまっていると多くの場合ここでaccess violationが起こり
アプリケーションが落ちてしまいます。

以上、GUIアプリケーションなどmrubyを扱う側で、mrubyのオブジェクトを保持する際の
gc付近の引っかかりやすいポイントについて書いてみました。
GUIアプリケーションでなくてもmrubyの外側でmrubyのオブジェクトを持つ場合には
同じようなことに遭遇しうるかと思います。
（サンプルはGUIアプリケーションではないですし。）
ご参考になれば幸いです。

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

```cpp
#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;
}
```