Imageオブジェクトの生成と解放

RubyGCはマーク&スウィープで、Ruby1.8時代からするとずいぶん改善されていてはいるが、それでもやっぱりGCが動くとプログラムは停止するし、逆にGCが動くまではオブジェクトは回収されない。
DXRubyを使うほうとしては初期のころはGCでずいぶん苦労したものだが、最近は特に意識することもない感じ。でもDXRubyを作るほうとしては意識せざるをえない。特にハードウェアリソースを扱うオブジェクトは気をつける必要がある。
今回はImageオブジェクトを題材としてGC周りについて。


Rubyの拡張ライブラリでクラスを作る場合、rb_define_class_under()を使ってクラスを生成し、クラスメソッドはrb_define_singleton_method()、initializeはrb_define_private_method()、インスタンスメソッドはrb_define_method()を使って定義する。もうひとつ大事なのは、initialize前に呼ばれるメモリ確保関数をrb_define_alloc_func()で定義することだ。

    /* Imageオブジェクトを生成した時にinitializeの前に呼ばれるメモリ割り当て関数登録 */
    rb_define_alloc_func( cImage, Image_allocate );

オブジェクトを生成したときに、Cで書いたinitializeが最初に呼ばれることは保証されない。継承したら呼ばれないこともある。Cで構造体を作って保持するオブジェクトの場合、initializeとは別に確実に呼ばれる関数が必要になる。それを定義するのがrb_define_alloc_func()なわけだ。
Image_allocate関数はこのような感じになっている。

/*--------------------------------------------------------------------
   Imageクラスのallocate。メモリを確保する為にinitialize前に呼ばれる。
 ---------------------------------------------------------------------*/
VALUE Image_allocate( VALUE klass )
{
    VALUE obj;
    struct DXRubyImage *image;

    /* DXRubyImageのメモリ取得&Imageオブジェクト生成 */
    image = malloc(sizeof(struct DXRubyImage));
    if( image == NULL ) rb_raise( eDXRubyError, "メモリの取得に失敗しました - Image_allocate" );
    obj = Data_Wrap_Struct(klass, 0, Image_release, image);

    /* とりあえずテクスチャオブジェクトはNULLにしておく */
    image->texture = NULL;

    return obj;
}

DXRubyImage構造体のメモリをmallocで確保して、Data_Wrap_StructマクロでRubyオブジェクト化する。allocateにはRubyでnewしたときの引数は渡ってこないから、生成する画像は不明で、ここではtextureにはNULLを入れることになる。最低限、initialize前にこれをやっておかないと、色々困ったことになるのだ。
ところでなぜRuby提供のメモリ管理を使わないのかという話だが、DXRubyはDirectXをスタティックリンクするせいで、いろいろスタティックリンクしてしまっていて、その都合で基本的には全部独自管理という形にしてしまった。Rubyが確保したメモリはDXRubyでfreeしないし、その逆もしない。そこいらへんを丁寧に作りこんであれば、RubyコンパイラとDXRubyのコンパイラが違うことでリンクするライブラリが異なっていても問題にはならない。
ダイナミックリンクにするとDirectXのバージョンが云々でエラーになって問い合わせが多くて面倒だったのだ。そのかわり、使うDirectX SDKはスタティックリンクできる最後のバージョンで2004年版とかいうちょー古いものになる。別に困ってないからいいんだけど。

んで、解放側の処理。上でRubyオブジェクト化するときにImage_releaseを指定しているので、GCに回収されるときにはImage_releaseが呼ばれる。Image_freeのほうはテクスチャが参照されているときに動く。

/*--------------------------------------------------------------------
   参照されなくなったときにGCから呼ばれる関数
 ---------------------------------------------------------------------*/
static void Image_free( struct DXRubyImage *image )
{
    /* テクスチャオブジェクトの開放 */
    image->texture->refcount--;
    if( image->texture->refcount == 0 )
    {
        RELEASE( image->texture->pD3DTexture );
        free( image->texture );
        image->texture = NULL;
    }
}

void Image_release( struct DXRubyImage *image )
{
    if( image->texture )
    {
        Image_free( image );
    }
    free( image );
    image = NULL;

    g_iRefAll--;
    if( g_iRefAll == 0 )
    {
        CoUninitialize();
    }
}

g_iRefAllはDircetXの終了処理をいつ行うかを管理するものだ。Rubyのシャットダウン処理でやればいいと思っていたのだが、オブジェクトの解放が実はその後で行われるために、シャットダウンでDirectXの終了処理をしてしまうとImageの解放などでコケるという現象が起こる。地味に苦肉の策。
image->textureがNULLになっているパターンはinitializeが呼ばれなかった場合と、disposeされたときとなる。disposeはImageオブジェクトは残した状態でテクスチャのみを解放する。

/*--------------------------------------------------------------------
   Imageクラスのdispose。
 ---------------------------------------------------------------------*/
VALUE Image_dispose( VALUE self )
{
    struct DXRubyImage *image = DXRUBY_GET_STRUCT( Image, self );
    DXRUBY_CHECK_DISPOSE( image, texture );
    Image_free( image );
    return self;
}


/*--------------------------------------------------------------------
   Imageクラスのdisposed?。
 ---------------------------------------------------------------------*/
static VALUE Image_check_disposed( VALUE self )
{
    if( DXRUBY_GET_STRUCT( Image, self )->texture == NULL )
    {
        return Qtrue;
    }

    return Qfalse;
}

textureをNULLにすることでdispose済みと判定するから、initializeする前はdispose状態という扱いになる。
ハードウェアリソースは有限なので、テクスチャデータは使わなくなったら解放したい。GCがその辺もうまいこと処理してくれればいいのだが、基本的にGCが管理するのはメインメモリなわけで、ビデオメモリはとりあえずユーザ責任で管理してくださいということになる。
テクスチャ作成時にエラーになったらGCを一度動かして再度チャレンジするのが理想的なのだが、面倒だったのでそこまではやっていない。