TypedDataについて

Ruby2.0でC拡張ライブラリ用のオブジェクト形式としてTypedDataが追加されている。俺はずっとRuby1.8&1.9用に作ってたからつい最近まで知らなかったのだが(新しいREADME.EXT.jaにも書いてないし)、RGenGCに対応する方法を追っかけてたらなんか出てきたという感じだ。今日ちょっといじってみたので把握したことをメモっておく。

■RData
昔はRData構造体を使ってて、バージョンによってちょっと違ったりするが、だいたいこんな感じの構造になっている。

struct RData {
    struct RBasic basic;
    void (*dmark)(void*);
    void (*dfree)(void*);
    void *data;
};

RBasicはフラグとクラスが入ったオブジェクトの基本構造だ。dmark、dfreeはCの関数ポインタを格納する。それぞれGCのマーク時、回収時にdataを引数にして呼ばれる。NULLにしておくと呼ばれない。dataはオブジェクトが保持するC構造体のポインタとなる。dfreeがNULLの場合はRubyがdataを解放してくれる。
RDataを扱う場合は自分でメモリを確保してData_Wrap_Structマクロを使うか、Data_Make_StructでRubyにメモリを確保してもらう。この方法で生成した場合、RData構造体の中にクラス情報が無いので、たとえばDXRubyのWindow.drawメソッドのようにImageオブジェクトを渡してもらったかどうかをチェックする手段が無い。クラス情報はRubyオブジェクトとしては持っているから、rb_obj_is_kind_of()などでクラス階層を辿ってチェックするのが伝統的だが、DXRubyではdfreeのアドレスで判定するというちょっと強引な方法を取っている。これをすると速いが、気をつけないとコンパイラの最適化でdfreeの関数が1つにまとめられて判定に失敗したりする。

■TypedData
Ruby2.0では内部でもTypedDataが使われているようだ。ある程度の添付ライブラリも対応している。していないのもある。TypedDataの構造体はこのようになっている。

struct RTypedData {
    struct RBasic basic;
    const rb_data_type_t *type;
    VALUE typed_flag; /* 1 or not */
    void *data;
};

dmarkとdfreeがrb_data_type_tという構造体のアドレスとtyped_flagに置き換わっている。サイズが同じでdataの位置も同じなので微妙に互換性がありそうだが、チェック関数がその違いをチェックしてエラーにしてくれる。チェックする手法はtyped_flagで、これはTypedDataのときに1になり、dfreeの関数ポインタで1はありえないという理屈である。
TypedDataを作るマクロはTypedData_Wrap_StructとTypedData_Make_Structで、型チェックはCheck_TypedStruct、データの取り出しはTypedData_Get_Structを使う。これらのマクロに渡す型データはrb_data_type_t構造体のポインタで、以下のような構造体をグローバルに定義してそのアドレスを指定する。

typedef struct rb_data_type_struct rb_data_type_t;

struct rb_data_type_struct {
    const char *wrap_struct_name;
    struct {
        void (*dmark)(void*);
        void (*dfree)(void*);
        size_t (*dsize)(const void *);
        void *reserved[2]; /* For future extension.
                              This array *must* be filled with ZERO. */
    } function;
    const rb_data_type_t *parent;
    void *data;        /* This area can be used for any purpose
                          by a programmer who define the type. */
};

Ruby2.1では最後にフラグが追加されていて、そのフラグにFL_WB_PROTECTEDを設定してやればRGenGCでShady化されなくなる。これを立てるとdata内のVALUE値の変更箇所を必死に探してライトバリアを埋め込む作業が始まる。
生成マクロに渡すrb_data_type_t構造体はRTypedData構造体内にアドレスで保持されるので、これをローカル変数などで作ってはいけない。mallocで確保するのはアリかもしれないが解放すべきタイミングが不明で、やっぱりグローバルで定義しておくのが手堅い。型チェック時にはアドレス比較で判定され、中身がまったく同じ構造体を作って渡しても違うと判定される。
wrap_struct_nameは構造体名称の文字列。dmark、dfreeはRDataと同じ。dsizeはObjectSpaceで使われる使用メモリサイズを返す関数。reservedは0固定。parentはスーパークラスがある場合にそれのアドレスを設定する。
C構造体を持つクラスをCで継承した場合、サブクラス側のC構造体はスーパークラスと同じデータ構造にして後ろに追加データをくっつけるような形となる。と思う。Cでオブジェクト指向するときの常套手段だ。クラスを継承した場合、スーパークラスとサブクラスでrb_data_type_t構造体のアドレスが違うため、kind_ofの判定がそのままではできない。parentを設定しておくと自動でチェックしてくれる、というわけだ。ちなみにRuby本体のどっかで使ってたはずなので調べてみれば実例が見られるはずだ。
TypedDataを使うとクラスのチェックが高速にできるのでRDataを使っていたときのような苦労は無くなる。

■おしまい
RData構造体とTypedData構造体のどっちを使った場合でもRubyから見るとT_DATA型のオブジェクトとなる。typed_flagで判定されるのでマクロを混在させるとエラーになる。
古いコードがコンパイルできるのは大変良いことだが、今後TypedDataを推奨していくのならREADME.EXT.jaには古いマクロではなく新しいマクロを書いておくべきだし、本来ならRuby2.0の時点でそうなっていなければならなかったはずだ。とはいえ、そうすると例題のdbmパッケージの作り方から直さないといけなくなってきたりして意外に変更が大きい。ともあれ、TypedDataを推奨しているのかどうかといったあたりの方針が不明なのでなんとも言えない。なんせTypedDataの存在自体を今まで知らんかったぐらいだし。
README.EXT.jaにTypedDataを書くとしても、RGenGC関連については「拡張ライブラリの作り方」とは少し離れた話であろうと思うので、突っ込んで説明することは無さそうだ。README.EXT.jaはRGenGCの動作を説明する文書じゃないからだ。
README.EXT.jaの修正案を出してみようと思って考えてはいるのだが、どうも言うほど簡単でもなさそうである。