DEV Community

SUZUKI Tetsuya
SUZUKI Tetsuya

Posted on

リソースオブジェクトについて

※この記事は 2014年12月17日 に Qiita に投稿したものです。 crypto に関する記述は Erlang 17.3 時点で、現在の最新版では仕様が変わっています。


リソースオブジェクトとは

NIF で使われるデータ型の一つにリソースオブジェクトがあります。リソースオブジェクトとは任意のデータをラップして Erlang との橋渡しをするデータで、主に構造体へのポインタをラップするのに使われます。 Erlang レベルにリソースオブジェクトを渡す場合は、リソースオブジェクトを持つリソース項を生成して渡します。 Erlang レベルからリソースオブジェクトを受け取るには、受け取ったリソース項からリソースオブジェクトを取り出します。ややこしいですね。

リソース項はなぜか Erlang レベルではバイナリ型とみなされますが (is_binary/1 が真になる) 、内容は隠蔽されて見えなくなります。また、ラップするデータごとにリソース型 (resource type) を用意しておくことで、引数として受け取ったリソースオブジェクトを区別でき、不正な型のデータが NIF に渡ってしまうのを防げます。

リソースオブジェクトを使わない場合

NIF レベルで使うデータを Erlang レベルに渡したい場合、数値や文字列のような単純なデータなら、ストレートに Erlang 項に変換すれば簡単です。値の妥当性検査も簡単です。

問題はバイナリや構造体のような検査しにくいデータを Erlang レベルに渡す場合で、データをバイナリ項で表したり、ポインタ値を整数項にするなどで Erlang レベルに露出させてしまうと VM が落ちる原因になる可能性があります。

Erlang の項はすべて不変ですので項の内容は変更できませんし、直ちに問題が起こるわけではありません。問題は、項を NIF レベルで処理するときに起こります。

例えば crypto モジュールがこの問題を抱えています (抱えているっつーかほったらかしっつーか) 。対話型シェルで次のコードを入力すると VM が落ちるはずです。 (17.3 で確認)

$ erl
Erlang/OTP 17 [erts-6.2] [source] [64-bit] [smp:12:12] [async-threads:10] [kernel-poll:false]

Eshell V6.2  (abort with ^G)
1> {sha256, C1} = crypto:hash_init(sha256). 
{sha256,<<103,230,9,106,133,174,103,187,114,243,110,60,
          58,245,79,165,127,82,14,81,140,104,5,155,171,
          217,131,...>>}
2> C2 = list_to_binary(lists:reverse(binary_to_list(C1))).
<<0,0,0,32,0,0,0,0,0,0,0,0,0,0,19,204,0,0,0,1,17,116,12,
  216,0,0,0,1,0,...>>
3> crypto:hash_update({sha256, C2}, <<123>>).
Segmentation fault: 11

これでなぜ落ちるのかというと、 hash_update/2 が受け取ったバイナリが、 OpenSSL 内部で使われる構造体のデータとしてストレートに OpenSSL の関数に渡ってしまうからです。これを防ぐには引数の内容をチェックするか、そんなことはめんどくさいし完璧にはできないので、リソースオブジェクトを使うと安全です。

前述の crypto モジュールでは、 HMAC の関数でのみリソースオブジェクトが使われています。 hmac_init/2 を実行すると、表面上は空のバイナリに見えるデータが返ってきます。

11> C = crypto:hmac_init(sha256, <<>>).                   
<<>>
12> is_binary(C).
true

リソースオブジェクトの操作

メモリ管理

リソースオブジェクトのメモリ管理は参照カウントで行われます。 Erlang 項ではないため GC には回収されません。

     Erlang レベル                           NIF レベル
<---------------------->       <---------------------------------->
リソース項 (ERL_NIF_TERM) <----> リソースオブジェクト <----> 任意のデータ
    GC に回収される                 参照カウント
  1. リソースオブジェクトから生成したリソース項、これは Erlang のオブジェクトなので GC に回収されます。
  2. リソース項が解放されると、リソース項が保持していたリソースオブジェクトへのオーナーシップが取り消されます。要は参照カウントが一つ減ります。
  3. リソースオブジェクトは参照カウントが 0 になると解放されます。
  4. リソースオブジェクトが解放されるとデストラクタ関数 (enif_open_resource_type() で指定します) が呼ばれ、ラップされていたデータの解放処理が行われます。

参照カウントの増減するタイミングはこうです:

  • リソースオブジェクトを生成すると (enif_alloc_resource) 、カウントは 1 になる
  • リソース項を生成すると (enif_make_resource) 、カウントが 1 増える
  • enif_keep_resource を呼ぶと、カウントが 1 増える
  • enif_release_resource を呼ぶと、カウントが 1 減る
  • リソース項が GC に回収されると、カウントが 1 減る

増減のタイミングはいくつもありますが、基本的には「 alloc/keep したらどこかで release する」です。もちろん、解放したくなければ release する必要はありません。

リソース型を生成する

リソースオブジェクトの準備として、まずはリソースオブジェクトの内容を区別するためのリソース型を enif_open_resource_type で生成しておきます。 rebar で NIF のテンプレートを指定してプロジェクトファイルを作ると、リソース型を生成するコードも含まれます。

こんな感じ。 enif_open_resource_type は特定のコールバック (load, reload, upgrade) でのみ呼べます。

static ErlNifResourceType *rt;

static int
on_load(ErlNifEnv* env, void** priv_data, ERL_NIF_TERM load_info)
{
  ErlNifResourceFlags flags = ERL_NIF_RT_CREATE | ERL_NIF_RT_TAKEOVER;
  rt = enif_open_resource_type(env, NULL,
      "resource_name",   /* リソース名 */
      &resource_cleanup, /* リソースの解放処理を行う関数 */
      flags, NULL);
  if (rt == NULL)
    return -1;

  return 0;
}

リソースオブジェクトを生成する

リソースオブジェクトの生成から破棄までが一つの関数内で完結するような単純な処理であれば、だいたい次の流れで書けます。

  1. リソースオブジェクトを生成する (enif_alloc_resource) -> 参照カウントは 1
  2. リソース項を生成する (enif_make_resource) -> 参照カウントは増えて 2
  3. リソースオブジェクトの参照を減らす (enif_release_resource) -> 参照カウントは 1 。リソース項が GC に回収されると 0 になる
  4. リソース項を返す

次のコードはドキュメントに掲載されているサンプルです。

{
    ERL_NIF_TERM term;

    /* リソースオブジェクトを生成する */
    MyStruct* obj = enif_alloc_resource(my_resource_type, sizeof(MyStruct));

    /* 構造体を初期化する ... */

    /* リソース項を生成する */
    term = enif_make_resource(env, obj);

    if (keep_a_reference_of_our_own) {
        /* 'obj' を静的変数に代入するなら release の必要はない */
    }
    else {
        /* リソースオブジェクトの参照カウントを減らす */
        enif_release_resource(obj);
    }
    return term;
}

これまでリソースオブジェクトがリソースオブジェクトがーと連呼してきましたが、リソースオブジェクトそのものを示す型は NIF では登場しません。 enif_alloc_resource が返すのは、指定したサイズのメモリブロックへのポインタ (void *) です。実行中の参照カウントの値を知る方法もありませんので、参照カウントではまるとデバッグが面倒です。

リソース項からデータを取得する

リソース型を指定し、 enif_get_resource で取得します。指定のリソース型以外の型はすべて弾かれます。 Erlang レベルではリソース項を改竄できませんから、これで引数にどんな値が来ても大丈夫です。

static ERL_NIF_TERM my_func(ErlNifEnv* env, int argc, const ERL_NIF_TERM a
rgv[])
{
  my_resource *data;
  if (!enif_get_resource(env, argv[0], my_resource_type, (void **)&data))
    return 0;
    ...
}

まとめ

君も crypto モジュールを修正すれば Erlang/OTP に contribute できるチャンス!...だと思いますがどなたかどうですか。私はめんどくさいのでいいです。

Top comments (0)