アンマネージコードにC#のデリゲートを渡す つづき

前回の記事でアンマネージコードにC#のデリゲートを渡して実行してもらう方法を説明しました。


その中でC,C++の関数ポインタに__stdcallをつけなければならないと書きましたが、C#のデリゲートを__cdeclにする方法が.netにはあったらしいのです。
方法は前回の記事のコメントに書いてあるのでそちらを参照してください。
情報提供ありがとうございました。


さて。前回この記事の本題はまた次回にと書いたので今回はその続きです。
デリゲートをアンマネージコードに渡す際に注意しなければならないことです。
それはズバリ、デリゲートの寿命管理です


C#のデリゲートは型でありインスタンス化することで使用します。
インスタンスであるというこはガベージコレクション(:以降GC)の対象になるということです。
GCはたとえアンマネージ側がC#から渡されたデリゲートを持っていたとしても、それを直接検出する方法を持っていません。
なので何かの拍子でC#側のデリゲートインスタンスの参照が外れGCに回収されたデリゲートをアンマネージ側が呼び出した場合、そこに待ち受けるのは悲劇のみです。


前回の使い方で大丈夫な場合というのは、アンマネージコードに渡したデリゲートが呼出し中でしか使われずアンマネージコートが制御を返した時点でもうそのデリゲートがアンマネージ側から呼ばれないという前提がある場合のみです。
前回のEnumWindowsはその前提が成り立っているのでデリゲートの寿命を考えることなく使えたわけです。
しかしWin32APIのSetTimerのコールバック関数にデリゲートを渡した場合、そのデリゲートは別のタイミングで呼び出されるのでデリゲートの寿命管理をしなければなりません。(そもそもWin32APIのSetTimerをC#から呼びだす利点はありませんが。ちょうどいい例がほかに思いつかなかった。。。)


肝心の寿命管理ですが基本的な考えは単純です。
渡したデリゲートがアンマネージ側から呼ばれなくなるまで参照をずっと持っていればいいのです。
一番簡単な方法はクラスのインスタンスやstaticなインスタンスとして参照を持っておくことです。
もちろんクラスのインスタンスとして持った場合、アンマネージ側から呼ばれなくなるまでそのクラスのインスタンスそのものも生きていなければなりません。


しかし、アンマネージコード呼び出しから制御が返ったあともデリゲートは呼び出されるが、同じメソッドの終了時に別のアンマネージコード呼び出しによりデリゲートの呼び出しを止める、
ということがあまりないかもしれませんが無きにしも非ず、です。少なくとも僕はありました。


たとえば先ほどのSetTimerにデリゲートを渡すことで別のタイミングでそのデリゲートは呼び出されます。そして同じメソッド内でKillTimerで呼び出しを止めます。
一連の処理は一つのメソッド内で完結しているわけなのでデリゲートのインスタンスもそのメソッドのローカル変数として宣言したいところです。
実際僕はそうしました。(例にあるSetTimerとKillTimerを使っているわけじゃありませんが)
メソッドの最初のほうでデリゲートをインスタンス化しアンマネージコードに渡します。
そしてメソッド終了時に再び別なアンマネージコードを呼び出して、デリゲート呼び出しを止めます。


まずDebugビルドで動かします→完璧です。
次にReleaseビルドで動かします→あぼん。


何度やってもDebugでは動くがReleaseでは動きません。
デリゲートの参照がどうにかなってるのかなーということは予想できたのですがどうなっているのかわからない。
実際クラスのインスタンス変数として持った場合ちゃん動きます。
さてこれは困ったぞと。
困っちゃったので調べました。
そして発見しました。


詳しいことを説明すると長くなるので簡単にだけ。
たとえばデリゲートをアンマネージに設定したあとGCが動き出したとします。
そしてアンマネージ側に設定したデリゲートがC#のコードでそれ以降利用されていない場合、
Releaseの場合そのデリゲートは回収されてしまいます。
たとえアンマネージ側でデリゲートが利用されていたとしてもGCはそれを知らないので問答無用で回収してしまいます。
しかしDebugの場合はそれは回収されません。理由はブレークポイントを設定して実行を止め変数の中身をウォッチできるようにするためです。
このような仕様のためReleaseでは動きDebugでは動くというわけでした。


このような仕様ならば、すべてインスタンス変数に持たなければいけないのでしょうか?
→Noです。


もちろん方法はあります。
このような場合GCにまだ回収するなと伝えるためには、System.Runtime.InteropServices名前空間にあるGCHandleという型を利用します。
GCHandleのstaticメソッドのAllocの引数が一つだけのもの、もしくは二つ目の引数にGCHandleType.Normalを指定します。
そしてAllocの第一引数にデリゲートを渡すことによってそのデリゲートはGCの対象から外れます。
Allocの第一引数に渡す型はobjectになっているのでデリゲート以外にも任意の型を同じように指定すればGCの対象から外すことができます。
そして使い終わればGCHandleのインスタンスメソッドのfreeを呼び出して元に戻します。


こうすることによってデリゲートの寿命を管理することができるようになりました。
もちろんReleaseでもちゃんと動きます。


ちなみにGCHandleTypeに別なものを指定すればGCHandleは別な挙動をします。
知っていると便利なことがあると思うのでGCHandleについて一度調べてみることをお勧めします。(←毎度おなじみ他力本願)


このようにアンマネージコードにデリゲートを渡す際にはいろいろと注意が必要になります。
以上でこのトピックスについて僕の知っているすべてなのですが、もう少し書きたいことがあるのでそれはまた次回。
何やらやたらと記事が長くなってしまう癖があるみたい。