mruby で C 言語の構造体をラップしたオブジェクトを作る正しい方法

  • 50
    いいね
  • 0
    コメント

mruby で C 言語の構造体をラップしたオブジェクトを作る正しい方法

※ この投稿は mruby Advent Calendar 2013 の記事として書かれたものを、内容を mruby 1.3.0 に合わせて更新したものです。

mruby は小さな言語処理系です。それ単体では HTTP 通信や JSON の読み書きはおろか、ファイル I/O すらできません。Ruby の処理系なのに正規表現も使えません。しかし、mrbgem と呼ばれる拡張モジュールを組み込むことで、必要な機能を必要なだけ足してやることができます。

mrbgem の開発はかんたんです。mrbgem は C と Ruby (mruby) のコードの組み合わせで書くことができます。mruby の既存の機能で実現可能な範囲のコードは Ruby で書き、Ruby では書けないような処理は C で書くことが機能性や保守性の面からは有利です。

mrbgem の開発においては、既存の C 言語ライブラリが積極的に再利用されています。mgem-list という mruby の gem のリストをざっと眺めてみても、C 言語のライブラリを利用していないものを探す方が難しいくらいです。

C 言語のライブラリでは、ライブラリ固有の情報や状態などを C 言語の構造体にパックしてそれを使い回すテクニックがよく使われます。例えば C 言語の標準入出力ライブラリは、ファイルの状態を FILE 構造体として表現します。標準入出力ライブラリの利用者は FILE 構造体の中身を意識することはありませんし、また中身を直接アクセスすることは推奨されません。そして C 言語のコードの中で FILE 構造体をやりとりする場合は、FILE 構造体へのポインタを関数の引数やグローバル変数として受け渡します。C 言語のコードに閉じている場合はそれで事足りますが、mruby の拡張モジュールでは C 言語で書かれたコードと Ruby で書かれたコードを行き来する必要があるため、単にどこかの変数に格納するだけは済みません。

C 言語のコードと Ruby のコードを行ったり来たりしつつ、C 言語ライブラリを使うための構造体を保持し続けるためには、C 言語ライブラリが要求する構造体に皮をかぶせて Ruby のオブジェクトとしてしまうのが一番シンプルな方法です。mruby では、このようなオブジェクト (適当な名前が無いようなので以下 Data オブジェクトと呼びます) を扱うクラスをたいへん簡単に作ることができます。しかしこれを 正しく 使うのは意外とやっかいです。

この記事では、この Data オブジェクトを正しく扱う方法を、Data オブジェクトを扱う mrbgem を作りながら解説します。

mrbgem を作る

公式のドキュメント に従いつつ mrbgem を作ってみます。mrbgem の名前は "mruby-hoge" にします。

まず mrbgem の名前でディレクトリを作り、その中にソースコードを格納するディレクトリを作ります。

% cd mruby
% mkdir mruby-hoge
% mkdir mruby-hoge/src

mrbgem をビルドするため、build_config.rb に設定を追記します。必要なビルドターゲットそれぞれに追加してください。典型的には、先頭の MRuby::Build.new ブロックと、ユニットテストのための MRuby::Build.new('test') ブロックに追加します。

build_config.rb
MRuby::Build.new do |conf|
  ...
  conf.gem "#{root}/mruby-hoge"
  ...
end

MRuby::Build.new('test') do |conf|
  ...
  conf.gem "#{root}/mruby-hoge"
  ...
end

続けて mruby-hoge/mrbgem.rake を、テンプレート からコピーして作ります。ひとまずは名前だけ書き換えれば十分です。

mruby-hoge/mrbgem.rake
MRuby::Gem::Specification.new('mruby-hoge') do |spec|
  spec.license = 'MIT'
  spec.author  = 'mruby developers'
  spec.summary = 'Example mrbgem using C and ruby'
end                                   

そして mruby-hoge/src/hoge.c に本体のコードを書きます。まずは何もしないものを置きます。

mruby-hoge/src/hoge.c
#include "mruby.h"

void
mrb_mruby_hoge_gem_init(mrb_state *mrb)
{
}

void
mrb_mruby_hoge_gem_final(mrb_state *mrb)
{
}

ここまで書けばコンパイルが通ります。

% rake
CC    src/array.c -> build/host/src/array.o
CC    src/backtrace.c -> build/host/src/backtrace.o
...
CC    mruby-hoge/src/hoge.c -> build/host/mrbgems/mruby-hoge/src/hoge.o
...
Build summary:

================================================
      Config Name: host
 Output Directory: build/host
         Binaries: mrbc
    Included Gems:
             mruby-hoge - Example mrbgem using C and ruby
             ...
%

はい、これで何もしない mrbgem ができました。

Note: mruby の拡張モジュールを C で書く方法については @cubicdaiya さんの mrubyとCの連携 という Post が参考になります。

構造体をラップしたオブジェクトを作る

さて、準備ができたのでC構造体をラップしたオブジェクトを作ってみます。まずはてきとうにC構造体を用意します。ただしメンバにポインタを含むと話がややこしくなるので、まずはポインタを含まない構造体にしておきます。

mruby-hoge/src/hoge.c
struct hoge {
  int num;
};

そしてクラスを定義します。名前は Hoge にします。

mruby-hoge/src/hoge.c
#include "mruby/class.h"
#include "mruby/value.h"

void
mrb_mruby_hoge_gem_init(mrb_state *mrb)
{
  struct RClass *cls;

  cls = mrb_define_class(mrb, "Hoge", mrb->object_class);
  MRB_SET_INSTANCE_TT(cls, MRB_TT_DATA);
  mrb_define_method(mrb, cls, "initialize", mrb_hoge_init, MRB_ARGS_NONE());
}

クラスの作成には mrb_define_class を使います。この時、親クラスに Object クラスを指定しているのが第一のポイントです。Data クラスは基本的に Object 以外の組み込みクラスの子孫にはできません。ガベージコレクション(GC)のコードが、Data クラスのオブジェクトは Object クラスのオブジェクトと同じメモリレイアウトであると仮定しているためです。

次に

  MRB_SET_INSTANCE_TT(cls, MRB_TT_DATA);

で、いま作ったクラスのインスタンスのデータタイプを MRB_TT_DATA と宣言します。このデータタイプは GC 時の後始末など、オブジェクトの取扱いに影響します。最後に initialize メソッドを mrb_hoge_init 関数で定義します。なお、ここまで struct hoge が出てきていませんが、Hoge クラスと struct hoge の紐付けはコンストラクタ mrb_hoge_init の中で行われます。

Note: 本稿で作成した mrbgem では、mrbgem の名前と構造体名と関数名にすべて "hoge" を入れています。これは単なる慣例でルールではないため、任意の名前に変えても動きます。ただし mrbgem の名前と mrbgem の初期化/終了関数の名前だけは例外的に強い依存関係があり、ビルドシステムによって mrb_#{mrbgemの名前}_gem_init/final と決められており変更できません。

さて、mrb_hoge_init の中身を書く前にもうひとつ準備をしておきます。

mruby-hoge/src/hoge.c
#include "mruby/data.h"

const static struct mrb_data_type mrb_hoge_type = { "Hoge", mrb_free };

struct mrb_data_type 型の変数 mrb_hoge_type を定義します。struct mrb_data_type は、Data オブジェクトが GC で回収される時に後始末をする関数を定義するためのデータ構造です。struct mrb_data_type の定義は mruby/data.h にあります:

include/mruby/data.h
22 typedef struct mrb_data_type {
23   /** data type name */
24   const char *struct_name;
25 
26   /** data type release function pointer */
27   void (*dfree)(mrb_state *mrb, void*);
28 } mrb_data_type;

struct_name は例外のメッセージとして表示するためにしか使われないため適当で構いません。そのクラスの名前を書くのが慣例です。一方もうひとつのメンバ dfree は重要です。dfree は関数ポインタで、その関数は Data オブジェクトが GC によって到達不能(=ゴミ)と判定された時に呼ばれ、malloc した領域やファイルデスクリプタの解放などのオブジェクトの後始末を行います。

さて Hoge クラスの mrb_data_type を改めて見てみます。

const static struct mrb_data_type mrb_hoge_type = { "Hoge", mrb_free };

mrb_hoge_type は変更する必要がないため const で宣言しています。はじめのメンバ struct_name は慣例に従ってクラスの名前と同じ "Hoge" としました。そして dfree には mruby のメモリ管理 API をそのまま使って mrb_free を設定しました。後述のように構造体の領域を mrb_malloc を使って確保する Data オブジェクトで、また構造体メンバにポインタなどの解放が必要なリソースを含まない場合は mrb_free を設定しておけば十分です。

Note: ポインタを含まなくても、ファイルデスクリプタ等の解放処理が必要なリソースを含む場合はカスタム dfree を用意する必要があります。

準備ができたので mrb_hoge_init を書きます。

mruby-hoge/src/hoge.c
mrb_value
mrb_hoge_init(mrb_state *mrb, mrb_value self)
{
  struct hoge *h;

  h = (struct hoge *)mrb_malloc(mrb, sizeof(struct hoge));
  h->num = 1234;
  DATA_TYPE(self) = &mrb_hoge_type;
  DATA_PTR(self) = h;
  return self;
}

一行づつ見てゆきます。

まず mrb_malloc で構造体のデータ領域を確保します。mrb_malloc は malloc(3) と同じように動きますが、 例外を上げる可能性がある 点が大きな違いです。 例外を上げる可能性があります。 ここ重要です。そして確保した h に適当に値をセットします。ここではメンバ num に 1234 でも入れておきます。
次に

  DATA_TYPE(self) = &mrb_hoge_type;

self のデータ型を mrb_hoge_type に設定し、

  DATA_PTR(self) = h;

でいま確保したデータ構造のポインタを self にセットします。ここまでで C 構造体をラップしたオブジェクトを作ることができるようになりました。

構造体の中身の参照

さきほどオブジェクトにポインタをセットするのに使った DATA_PTR は、反対にラップされたオブジェクトからポインタを取り出すことにも使えます。これを利用して値を取り出すメソッドを足してみます。

まず Hoge クラスにメソッドを定義します。名前はベタに #get メソッドとしておきます。

mruby-hoge/src/hoge.c
void
mrb_mruby_hoge_gem_init(mrb_state *mrb)
{
  ...
  mrb_define_method(mrb, cls, "get", mrb_hoge_get, MRB_ARGS_NONE());
}

#get メソッドの実装 mrb_hoge_get では、メンバ変数 num の値を Ruby オブジェクトに変換して返します。

mruby-hoge/src/hoge.c
mrb_value
mrb_hoge_get(mrb_state *mrb, mrb_value self)
{
  struct hoge *h;

  h = DATA_PTR(self);
  return mrb_fixnum_value(h->num);
}

いったん DATA_PTR でポインタを取り出せば、あとは通常の構造体ポインタとしてアクセスできます。ここでは h->num の値を mrb_fixnum_value 関数を使って Ruby の Fixnum オブジェクトに変換してリターンします。コンパイルして動かしてみましょう。

% rake 
...

% bin/mruby -e 'a = Hoge.new; p a.get'
1234

はい、バッチリですね。get を作ったので続けて put も作ってみます。

mrb_value
mrb_hoge_put(mrb_state *mrb, mrb_value self)
{
  struct hoge *h;
  mrb_int num;

  mrb_get_args(mrb, "i", &num);
  h = DATA_PTR(self);
  h->num = num;
  return mrb_fixnum_value(num);
}

void
mrb_mruby_hoge_gem_init(mrb_state *mrb)
{
  ...
  mrb_define_method(mrb, cls, "put", mrb_hoge_put, MRB_ARGS_REQ(1));
}

こちらはどうでしょうか。

% bin/mruby -e 'a = Hoge.new; a.put(3); p a.get'
3

うまく動いていますね。Hoge は Object クラスを継承しているので、Object クラスのメソッドもふつうに使え...

% bin/mruby -e 'a = Hoge.new; p a.object_id; p a.nil?; p a.dup.get'
1907878821
false
zsh: segmentation fault (core dumped)  bin/mruby -e 'a = Hoge.new; p a.object_id; p a.nil?; p a.dup.get'

おや?

#dup

segmentation fault が発生した原因を探ってみます。

% bin/mruby -e 'a = Hoge.new; a.dup'
% bin/mruby -e 'a = Hoge.new; a.dup.get'
zsh: segmentation fault (core dumped)  bin/mruby -e 'a = Hoge.new; a.dup.get'

#dup しただけなら大丈夫です。その後で #get を呼ぶと死にます。デバッガで追ってみます。

% gdb bin/mruby mruby.core
GNU gdb 6.3
...
Core was generated by `mruby'.
Program terminated with signal 11, Segmentation fault.
...
#0  0x0000074c0656528b in mrb_hoge_get (mrb=0x74eb1ca8600, self=
      {value = {f = 3.9684672274376703e-311, p = 0x74e286d38b0, i = 678246576, s
ym = 678246576}, tt = MRB_TT_DATA})
    at /home/tsahara/src/mruby/mruby-hoge/src/hoge.c:30
30        return mrb_fixnum_value(h->num);
(gdb)

mrb_fixnum_value 関数を使って h->num の値を Ruby のオブジェクトに変換しているところで死んでいます。まずはポインタ変数 h の値を見てみます。

(gdb) p h
$1 = (struct hoge *) 0x0

はい、NULL が入っていました。原因はこれですね。

では DATA_PTR(self) がなぜ NULL なのでしょうか。答えはかんたんで、誰も設定していないからです。Hoge#dup の実体は Object クラスの #dup メソッドです。しかし Object クラスのメソッドは子クラス側で DATA_PTR(self) を使っているなんて知りません。したがってここは子クラス側で面倒を見てやる必要があります。

Ruby (MRI) では、#dup メソッド等でオブジェクトのコピーが必要とされた場合には #initialize_copy が呼ばれます。mruby でも事情は一緒で、Data クラスには #initialize_copy メソッドを定義する必要があります。

mruby-hoge/src/hoge.c
static mrb_value
mrb_hoge_init_copy(mrb_state *mrb, mrb_value copy)
{
  mrb_value src;

  mrb_get_args(mrb, "o", &src);
  if (mrb_obj_equal(mrb, copy, src)) return copy;
  if (!mrb_obj_is_instance_of(mrb, src, mrb_obj_class(mrb, copy))) {
    mrb_raise(mrb, E_TYPE_ERROR, "wrong argument class");
  }
  if (!DATA_PTR(copy)) {
    DATA_PTR(copy) = (struct hoge *)mrb_malloc(mrb, sizeof(struct hoge));
    DATA_TYPE(copy) = &mrb_hoge_type;
  }
  *(struct hoge *)DATA_PTR(copy) = *(struct hoge *)DATA_PTR(src);
  return copy;
}

void
mrb_mruby_hoge_gem_init(mrb_state *mrb)
{
  ...
  mrb_define_method(mrb, cls, "initialize_copy", mrb_hoge_init_copy, MRB_ARGS_REQ(1));
}

理解しにくいコードが並んでいますが、注意しなければならないのは最後に構造体をコピーしている個所だけです。

  *(struct hoge *)DATA_PTR(copy) = *(struct hoge *)DATA_PTR(src);

あまりお目にかからない構造体の代入構文でまるっとコピーしています。struct hoge にはメンバが num しかありませんから、やっていることは結局 copy の num を src の num にコピーしているだけです。さて、直ったでしょうか。長くなるため今度は mirb で試してみます。

% bin/mirb
mirb - Embeddable Interactive Ruby Shell

This is a very early version, please test and report errors.
Thanks :)

> a = Hoge.new
 => #<Hoge:0xc7678ec9bb0>
> a.put(3)
 => 3
> b = a.dup
 => #<Hoge:0xc7678ec9a60>
> p b.get
3
 => 3
> b.put(4)
 => 4
> p b.get
4
 => 4
> p a.get
3
 => 3

うまく動いているようです。

カスタム dfree

それでは、もう少し複雑な構造体をラップしてみます。例としてファイル構造体 FILE * にしておきます。まず struct hoge にメンバ FILE *fp を足します。

mruby-hoge/src/hoge.c
#include <stdio.h>

struct hoge {
  int num;
  FILE *fp;
};

次に初期化処理 mrb_hoge_init に fp の初期化を足します。ファイル構造体を持つオブジェクトの初期化なので、コンストラクタ Hoge.new の引数としてファイル名を取るのが自然でしょう。そのファイルを fopen(3) に渡して、返り値で fp で初期化します。

mruby-hoge/src/hoge.c
#include "mruby/string.h"

mrb_value
mrb_hoge_init(mrb_state *mrb, mrb_value self)
{
  FILE *fp;
  struct hoge *h;
  mrb_value path;
  char *cpath;

  mrb_get_args(mrb, "S", &path);
  cpath = mrb_str_to_cstr(mrb, path);
  fp = fopen(cpath, "r");
  if (fp == NULL) {
    mrb_raisef(mrb, E_ARGUMENT_ERROR, "cannot open file: %S", path);
  }

  h = (struct hoge *)mrb_malloc(mrb, sizeof(struct hoge));
  h->num = 1234;
  h->fp = fp;
  DATA_TYPE(self) = &mrb_hoge_type;
  DATA_PTR(self) = h;
  return self;
}

Note: mrb_str_to_cstr は mruby の String を格納した mrb_value から C 言語の NUL 終端文字列を取り出すユーティリティ関数です。"mruby/string.h" で宣言されています。同様の機能を持つものに RSTRING_PTR マクロがありますが、こちらは適切な長さで NUL 終端されている保証がありません。ちょっと複雑なプログラムを書くと表れるという追い難いバグの原因となるため注意が必要です。

先に書いておくと、上のコードには バグがあります。 が、ひとまず先に解放のコードを書いてしまいましょう。解放する関数の名前は mrb_hoge_free としておきます。関数プロトタイプは、先述の通り "mruby/data.h" で定義されている

include/mruby/data.h
  void (*dfree)(mrb_state *mrb, void*);

に合わせます。mrb はおなじみの mrb_state へのポインタで、もうひとつの引数が C 言語構造体のデータを指し示すポインタです。実装はこの通りです:

mruby-hoge/src/hoge.c
static void
mrb_hoge_free(mrb_state *mrb, void *ptr)
{
  struct hoge *h = ptr;

  fclose(h->fp);
  mrb_free(mrb, h);
}

まずC 言語構造体のデータオブジェクトが持つリソース h->fp を解放し、それからデータオブジェクトそのもの h を解放します。そして mrb_hoge_type の dfree をいま定義した mrb_hoge_free 関数で置き換えます。

const static struct mrb_data_type mrb_hoge_type = { "Hoge", mrb_hoge_free };

それでは新しく追加した fp を使うメソッドを追加してみます。ファイルの内容を読み出す #read メソッドにします。きちんと機能するものを作るのは少々面倒なので「ファイルの先頭の方をたいてい読み出せる」メソッドにします。

mruby-hoge/src/hoge.c
mrb_value
mrb_hoge_read(mrb_state *mrb, mrb_value self)
{
  struct hoge *h;
  size_t n;
  char buf[1024];

  h = DATA_PTR(self);
  n = fread(buf, 1, sizeof(buf), h->fp);
  if (n == 0) {
    mrb_raise(mrb, E_ARGUMENT_ERROR, "fread(3) returns 0");
  }
  return mrb_str_new(mrb, buf, n);
}

void
mrb_mruby_hoge_gem_init(mrb_state *mrb)
{
  ...
  mrb_define_method(mrb, cls, "read", mrb_hoge_read, MRB_ARGS_NONE());
}

細かいことは気にせず fread(3) で 1024 バイトを読み出してみて、それを mruby の String オブジェクトにして返すことにしました。

% rake
% bin/mruby -e 'h = Hoge.new("mruby-hoge/mrbgem.rake"); puts h.read'
MRuby::Gem::Specification.new('mruby-hoge') do |spec|
  spec.license = 'MIT'
  spec.author  = 'mruby developers'
  spec.summary = 'Example mrbgem using C and ruby'
end
%

意図した通りに機能しているようです。

リソースの解放タイミング

残念ながら、現在の実装は大きなスクリプトで利用すると期待通りにうまく動きません。Hoge.new を何回も呼ぶと失敗する可能性があります:

% bin/mruby -e '10000.times { Hoge.new("mruby-hoge/mrbgem.rake") }'
trace:
        [0] -e:1
        [1] /home/tsahara/src/mruby/mrblib/numeric.rb:77:in times
        [2] -e:1:in call
-e:1: cannot open file: mruby-hoge/mrbgem.rake (ArgumentError)

繰り返しの回数をたとえば 10 回程度に減らしてやると動作します。回数が多くなると動かなくなります。原因は少々わかりにくいのですが、ファイルデスクリプタの不足が発生しています。ulimit -n 20 等としてリミットを小さくしてやると、ループの回数との相関が見てとれます。

Note: iij/mruby-errno を利用すると例外オブジェクトを介してもう少し詳細なエラーを取得できるため、システムコールを叩くような mrbgem のデバッグが楽になります。

でも、さきほど確かにファイル構造体を解放(fclose)する mrb_hoge_free を書きました。これが正しく呼ばれていないのでしょうか。

ポイントは mrb_hoge_free が呼ばれるタイミングにあります。mrb_hoge_free、一般化して dfree は Data オブジェクトが解放されるタイミングで呼ばれます。そして mruby ではオブジェクトが解放されるタイミングは GC が決めます。GC がオブジェクトを解放するタイミング... はややこしいので説明を省略しますが、ループから抜けた時に GC が走らない(ことがある)点がポイントです。そのために、ループ内で作成した Hoge オブジェクトはすぐには解放されず、その結果 Hoge オブジェクトが保持する FILE 構造体もクローズされず、プロセスのリミットに逹したところで fopen(3) が失敗し始めます。

この問題の対処は確保したリソースの特性にもよっていろいろ考えられます。今回は Ruby の MRI 実装 や iij/mruby-io で行われている対処を入れてみます。

mruby-hoge/src/hoge.c
  ...
  fp = fopen(cpath, "r");
  if (fp == NULL) {
    if (errno == EMFILE || errno == ENFILE) {
      mrb_full_gc(mrb);
      fp = fopen(cpath, "r");
    }
    if (fp == NULL) {
      mrb_raisef(mrb, E_ARGUMENT_ERROR, "cannot open file: %S", path);
    }
  }

  h = (struct hoge *)mrb_malloc(mrb, sizeof(struct hoge));
  ...

fopen(3) に失敗し、その原因がファイルデスクリプタ溢れだった場合は強制的に Full GC を走らせ、宙に浮いている Hoge オブジェクトがあればそれを回収することによって間接的に mrb_hoge_free を呼び、不要な FILE 構造体を fclose(3) するようにしました。

% bin/mruby -e '10000.times { Hoge.new("mruby-hoge/mrbgem.rake") }'
%

この通り動くようになりました。

リソースの設定順序

先に "カスタム dfree" の節でも書きましたが、現在の mrb_hoge_init の実装には バグがあります。 mrb_hoge_init を再掲します。バグは FILE 構造体のリークです。どういうロジックで起こり得るかわかりますか?

mruby-hoge/src/hoge.c
mrb_value
mrb_hoge_init(mrb_state *mrb, mrb_value self)
{
  FILE *fp;
  struct hoge *h;
  mrb_value path;
  char *cpath;

  mrb_get_args(mrb, "S", &path);
  cpath = mrb_str_to_cstr(mrb, path);
  fp = fopen(cpath, "r");
  if (fp == NULL) {
    if (errno == EMFILE || errno == ENFILE) {
      mrb_full_gc(mrb);
      fp = fopen(cpath, "r");
    }
    if (fp == NULL) {
      mrb_raisef(mrb, E_ARGUMENT_ERROR, "cannot open file: %S", path);
    }
  }

  h = (struct hoge *)mrb_malloc(mrb, sizeof(struct hoge));
  h->num = 1234;
  h->fp = fp;
  DATA_TYPE(self) = &mrb_hoge_type;
  DATA_PTR(self) = h;
  return self;
}

答えは mrb_malloc の呼び出しです。この位置で呼んではいけません。

C プログラマならぱっと見て、「NULL チェックしてないな」と気づくでしょう。しかしそれではありません(NULL チェックは必要と言えば必要ですが... 話が長くなるため割愛します)。実は、mrb_malloc は 例外を発生し longjmp(3) を使って一足飛びに mrb_hoge_init から抜け出してしまうことがあります。 ここの位置で抜け出されると、直前に開いた fp がコールスタックから消えさり解放しようが無くなります。

Note: どんな例外をどんな場合に上げるか興味のある方は src/gc.c からたどってください。

このバグはコマンドラインから mruby コマンドを実行するようなケースではほとんど問題にならないでしょう。しかし外部のプログラムに libmruby をリンクし、カスタムメモリアロケータ(mrb->allocf)を使ってメモリ容量に上限を設けておくようなケースでは、mruby 内でのリソースリークが組み込み先のプログラムを殺すことになります。

この問題はいくつかの方法で回避が可能です。今回のケースでは mrb_malloc の呼び出しを fopen の直前に持ってくるのがいちばん簡単です。しかしひとつの構造体の中に確保/解放が必要なリソースが複数あったり、また初期化関数の中で mrb_malloc() 以外の例外を発生させうる関数を呼ぶようなケースではとたんに難しくなります。よって、ここではもう少し一般的な方法を紹介します。

Note: 「例外を発生させうる関数」というのが曲者です。mruby の多種多様な API のうち、どの関数が例外を発生し得るかはその関数の実装をたんねんに追ってゆかないとわかりません。

まず mrb_malloc() による構造体のアロケートと DATA_TYPE/DATA_PTR のセットを先頭に持ってきます。

mruby-hoge/src/hoge.c
  ...
  char *cpath;

  h = (struct hoge *)mrb_malloc(mrb, sizeof(struct hoge));
  DATA_TYPE(self) = &mrb_hoge_type;
  DATA_PTR(self) = h;

  mrb_get_args(mrb, "S", &path);
  ...

これなら mrb_malloc() が例外を上げてもリークするリソースは何もありません。そして mrb_get_args の呼び出し以降で例外が上がっても(あるいは return で抜けても)、self には DATA_TYPE で GC 用の構造が設定済みですから、mrb_malloc() で確保した DATA_PTR(self) = h はいずれ GC によって解放されます。ただしこのままでは h->fp が初期化されないまま mrb_hoge_free が呼ばれる可能性があります。解放前は NULL で初期化しておくことにしましょう。

mruby-hoge/src/hoge.c
  ...
  h = (struct hoge *)mrb_malloc(mrb, sizeof(struct hoge));
  h->fp = NULL;
  DATA_TYPE(self) = &mrb_hoge_type;
  DATA_PTR(self) = h;
  ...

h->fp が NULL の場合でもちゃんと動くように mrb_hoge_free も更新します。

mruby-hoge/src/hoge.c
static void
mrb_hoge_free(mrb_state *mrb, void *ptr)
{
  struct hoge *h = ptr;

  if (h->fp != NULL) {
    fclose(h->fp);
    h->fp = NULL;
  }
  mrb_free(mrb, h);
}

このように mrb_malloc() で C 言語構造体をまずはじめに確保し、例外が発生しないことが担保されている間に NULL や -1 といった確保していないことを意味する値で埋めておき、いつどのタイミングで呼ばれてもリソースを適切に解放できるように dfree 関数を書いておくのがリソースリークを避けるコツです。

終わりに

本日は mruby で C 言語の構造体をラップしたオブジェクトを正しく取り扱う方法を紹介しました。この他にも、

  • TCPSocket のような複雑な #initialize を持つ Data クラスで、#initialize を Ruby のコードで実装する方法
  • Data クラスを継承した時の DATA_TYPE/DATA_PTR の初期化の仕方
  • SystemCallError のように .new の引数によって異なるクラスのオブジェクトを生成するクラスの実装法
  • リソースを持つ Data クラスの initialize_copy

など取り上げたいネタはありましたが、時間切れとなりましたのでこの記事はここでおしまいです。長い記事にお付き合いいただきありがとうございました。

この投稿は mruby Advent Calendar 201317日目の記事です。