FIELD NOTES: 書を持って街へ出よう

合同会社フィールドワークス プログラマ兼代表のブログ

OCamlでPython/Ruby拡張モジュールを作るときの注意点

Field Reports の拡張モジュールを作成する際に,メモリ管理やエラー処理といった通常サンプルプログラム等で説明されていような所で色々苦労しました。
注意点を記録しておきます。

メモリ管理

OCamlのGCと折り合いを付けるには,以下3点のルールを必ず守る必要があります。

  1. 関数の先頭に CAMLparamN() を置く。
  2. 関数内で allocate したデータを格納する変数は,CAMLlocalN() で宣言する。
  3. return文は使わずに,CAMLreturn() マクロを使用する。
CAMLparamN() マクロ

value型の引数がない場合は ,CAMLparam0() を使います。
value型の引数を受け取った場合は,CAMLparamN() の引数に含めます。

CAMLlocalN() マクロ

関数内で alloc したデータを一時的に変数に保持する必要がある場合は,そのローカル変数を CAMLlocalN() で宣言します。
alloc したデータを Store_field() 等で直接他のデータに格納する場合は必要ありません。
また,bool, int などの単純なデータの場合もCAMLlocalN() は不要です。

CAMLreturn() マクロ

関数の返り値がvoid型の場合は,CAMLreturn0 で処理を終わります。
返り値がvalue型の場合は,CAMLreturn() を使います。
value型以外の場合は,CAMLreturnT() を使います。

エラー処理

上記3ルールを原理的に守ろうとすると,エラー処理が若干窮屈になります。
例えばRubyの拡張モジュールでは,rb_exc_raise()でRubyの例外を発生させると CAMLreturn()マクロを通らなくなるので,関数を2段構えにしました。
例外発生時には,確保したもうデータを使わないので,そこまでやる必要はなかったかもしれません。

static VALUE caml_func(VALUE self, VALUE param, VALUE* exc)
{
    CAMLparam0();
    CAMLlocal1(result);

    result = ocaml_stub_func(convert_to_caml_data(param));
    if (Is_exception_result(result)) {
        *exc = convert_to_ruby_exception(Extract_exception(result));
    }

    CAMLreturnT(VALUE, result);
}

static VALUE rb_func(VALUE self, VALUE param)
{
    VALUE exc = Qnil;
    VALUE result = caml_func(self, param, &exc);
    if (exc != Qnil) rb_exc_raise(exc);
    return result;
}
問題発生時の対処

上記ルールのうち一箇所でもほころびがあると,大量のデータを処理した時や連続運用を行った時に segmentation fault や out of memory が発生します。
メモリ関係の障害は後々のGC発生のタイミングで顕在化しますので,問題の箇所を特定するのが非常に難しくなります。

結局対処としては,目を皿のようにして上記のルールが守られているかどうかをチェックするしかないと思います。

例外処理

例外の検出と例外オブジェクトの取得

OCamlのコールバック関数を呼ぶ際に caml_callback_exn() を使うと,OCaml内で例外が発生した場合に検出できるようになります。

呼び出し側では,Is_exception_result() により例外が発生したかどうかを判定できます。
また Extract_exception() で,例外オブジェクトを抽出できます。

使用例は,上記のサンプルコードを参照してください。

例外型の判定

例外オブジェクトの型をC側で判定し,文字列のメッセージを表示するために,以下のようにしました。

  1. Callback.register_exception で必要な例外をすべて登録する(OCaml側)。
  2. caml_named_value で例外データを一式取得する(以後C側)。
  3. 例外発生時に,例外オブジェクトを解析する。
    1. 例外の型を特定するために,例外オブジェクト(ブロック)の第1フィールドと caml_named_value で取得した例外を‘==’で比較する。
    2. 例外の文字列表現を取得するため,例外オブジェクトの第2フィールドを取得し,文字列に変換する。

マルチスレッド対策

OCamlのGCはマルチスレッドに対応していないため,OCaml関数処理中の再入を防ぐ必要があります。

OCaml関数の呼び出しを caml_leave_blocking_section() と caml_enter_blocking_section() ではさみました。

    caml_leave_blocking_section();

    ocaml_stub_func();

    caml_enter_blocking_section();

Pythonでは,マルチスレッド処理をしても拡張ライブラリを並列で呼ぶことはないようなのですが,念のため入れてあります。