thisが参照されない?

.NET GC & Interop クイズ.NET GC & Interop クイズ解答編 より

問題は Work メソッド内にある。Work メソッドが NativeMethod.WorkOnHandle を呼び出す時に
 this._handle をスタックに積む
 NativeMethod.WorkOnHandle
という手順を踏むが、この2つの手順の間で GC が発生した場合、this がすでにどこからも参照されていない可能性がある。

そんな可能性はないはずだ。
WorkOnHandle() メソッドを呼ぶためには、呼び出し元スレッドが this に相当する参照を操作する必要があり、その参照元は必ず Root 参照なので GC の回収対象にはならない。

ちょっと Interop だと違うらしい

microsoft.public.dotnet.framework.interop なんかでも指摘されているが、NativeMethod.WorkOnHandle() の中でスレッドハイジャッキング等が発生しない限り、GC によって this が回収されることはない。
WorkOnHandle() の中でマネージオブジェクトの管理しているウィンドウハンドルに SendMessage() したり、マーシャリングされた delegate を呼び出したりした場合かつ、WorkOnHandle() の呼び出し元がインスタンスを2度と触らないようなコード*1であったら、this は回収されてしまうようだ。

なんで Interop だけ?

当たり前のことだが、GC が回収対象とするかどうかという判断が、extern されたメソッドと managed なメソッドで違ったりはしないはず。
だから WorkOnHandle() がマネージドコードであっても、this は回収可能状態なわけで、call やら callvirt やら ret やらを通過した時点で、ハイジャックされたら回収されちゃうんじゃないの?
しかし、

new Thread(new ThreadStart(xxx)).Start();
new Form1().Show();

なんてしていて、Start() や Show() の中で this が回収されたりすることはない*2

そーれで、どうする?

この問題は、PInvoke の IntPtr の宣言を HandleRef に変更すると解決する。HandleRef のマーシャリング結果は IntPtr と同じなので一方的に渡すだけならお手軽である。
ああ、でも外に出してる*3コードは、いくつか HandleRef にしてるな、私(笑

HandleRef 構造体

プラットフォーム呼び出しを使用してマネージ オブジェクトを呼び出し、このプラットフォーム呼び出しの後でオブジェクトが他で一切参照されていない場合、ガベージ コレクタがマネージ オブジェクトを終了する可能性があります。これによって、リソースが解放され、ハンドルが無効になり、プラットフォーム呼び出しは失敗します。 HandleRef でハンドルをラップすると、プラットフォーム呼び出しが終了するまで、マネージ オブジェクトはガベージ コレクションの対象になることはありません。

まさにそのものですね。

*1:ようするに、そのメソッドが呼び出せた時点でインスタンスが回収可能になる状態

*2:こいつらは暗黙的にどこかに参照をつくっちゃいそうで、ちょっと特殊例かもしれないけど

*3:GDNJの投稿とか