「[.NET][COM] Marshal.ReleaseComObject の危険性について」 の記事へのコメントで Jitta さんに 「More on ReleaseComObject (and why we did not implement IDisposable on the classes contained in the RCW)」 という続き的な記事があることを教えてもらいました。
せっかくなのでこちらも軽く紹介。
[厳密な訳ではありません。かなり大雑把ですし、一部英文の意味がよくわからないところもあります。正確なところはぜひ原文をご覧ください。なお、文字色や区切り線は原文にあわせてあります]
RCW に IDisposable を実装しなかったのは RCW が積極的に COM オブジェクトを Release することにリスクがあるからだ。それらのリスクの詳細は Yves blog link を参照して欲しい。
けど、それから誰かに聞かれた。
OK。RCW が IDisposable を使わないと決めたことに異議は無い。必要無いときに ReleaseComObject を呼ぶべきではないということにも同意する。
しかし、他の 2つの点について同意できないことがある。
RCW への参照を保持しているコードを ReleaseComObject したあとに使うことができないということ。
あるスレッドが ReleaseComObject を呼ぶときに他のスレッドが同じオブジェクトを使っていると、AV [アクセス違反のこと] が発生したりメモリがおかしくなったりすること。
#1 (前者のこと) は COM オブジェクトに限ったことではない。アンマネージリソースをカプセル化しているオブジェクトには同じことが言えるはず。たとえば、FileStream オブジェクトはクローズしたあとにリードやライトをすることはできない。しかし、FileStream は IDisposable を実装できないということはない。
#2 (後者) は私には理解できない。ReleaseComObject は IUnknown::Release を一度呼び出すんだよね?そうであれば、他のスレッドにどう影響するんだ?COM インターフェースをスレッド間でマーシャリングするとき CoMarshalInterThreadInterfaceInStream を呼びそれから CoGetInterfraceAndReleaseStream を呼ぶ。この結果オブジェクトの参照カウンタは一つインクリメントされる。オブジェクトがフリースレッドモデルで同じマルチスレッドアパートメントに属す 2つのスレッド間でマーシャリングするときは、確かに物理的に同じポインタを受け取ることにはなるが、その場合であっても参照カウンタはインクリメントされる。あるスレッドが ReleaseComObject を呼び、別のスレッドがそのオブジェクトを使っていたとしよう。ReleaseComObject の結果として参照カウンタは 1つデクリメントされるけれども、オブジェクトはまだ生きてるし、2番目のスレッドは普通に動く。なぜ AV やメモリがおかしくなると言ったことになるのか理解できない。
Dave の追加説明。
#1 について。他のマネージドコンポーネントも同じじゃないかということは正しい。しかし、RCW 自身が持っている COM コンポーネントを呼び出すたびに切断されていないかどうかをチェックするというコストを払いたくない。そのようにスタブは最適化されている。RCW はこの最適化された x86 スタブを通じてメソッドを呼び出すので、すでに ReleaseComObject を呼ばれている場合には AV の原因となり得る。こういったわけで、IDisposable を実装してるほとんどのマネージドコンポーネントの振る舞いよりもひどいことになってしまう。
#2 について。両スレッドが MTA ならばプロキシを取得する必要はないし、パフォーマンス的な理由で呼び出しのたびに COM コンポーネントの AddRef や Release を呼び出したりしない。この理由で、MTA スレッドから ReleaseComObject を呼び出すと別の MTA スレッドでメソッドを呼び出したときに COM コンポーネント内部で AV が発生したりプロセス全体の状態がおかしくなったりする。
結局のところ以下のような話なのかと思いました。
#1 の方を前回の記事と合わせて考えると、要するに 「ReleaseComObject でも IDisposable でもいいけど、もうちょっとうまいことできないのか?」 という話に対して 「COM の呼び出し部分ってなかなかそううまくいかない。だから IDisposable はやめた。ReleaseComObject は用意しておくけど不用意に使うと死ぬよ。だからコード書く人が十分に気を付けて ReleaseComObject を使うか、もしくは反対に何もせずにランタイムに任すかして欲しい。ランタイムに任せたときは GC によって解放された時かプロセスが終了するときに Release されることになるよ」
#2 の方はそれなりに COM のことがわかってないと意味わからないかもしれませんね。
COM の仕様上、呼び出し元も呼び出し先も MTA (と言うか、フリースレッドと言った方がいいのかな?) ならばマーシャリングする必要はありません。同じポインタを使い回して問題ありません。これがプロキシを取得する必要が無い理由です。
MTA の場合はスレッドをまたいでも同じポインタを使い回しているということなわけですから、結局のところスレッドまたぎかそうでないかはまったく関係ない話になって、#1 と同じ話ということになるわけです。
じゃ、MTA じゃない場合は?
この場合にスレッドを超えようと思うと COM インターフェースポインタのマーシャリングが必要になるし、スレッド間の通信は実はメッセージポンプがベースだったりとか COM の一番ややこしい世界に入っていきます。
なので、RCW がどうやって管理してるのかなんて私にはさっぱりわかりませんw
と言うか、あらためて考えてみたら、たとえば STA な COM インターフェースポインタを持っている RCW があったとして、その RCW がマネージドな世界で別のスレッドに渡された場合っていったいどのタイミングで CoMarshalInterThreadInterfaceInStream、CoGetInterfaceAndReleaseStream してるんだろう?
あれ?
RCW が自動的にこれらをやってくれるってことは無いのかな?普通の方法ではどうやっても無理なような気がする。。。
ひょっとして、Marshal.GetComInterfaceForObject なりでインターフェースポインタを取り出して自分で CoMarshalInterThreadInterfaceInStream、CoGetInterfaceAndReleaseStream してやらないといけないのかな?
7/8 19:50 追記
さっそく渋木さん、菊池さんからコメントとトラックバックを頂きました。(ありがとうございます)
MTA じゃない場合、というか、COM インターフェースポインタのマーシャリングについてはやはり RCW は一切何もしてくれないそうです。
両スレッドが MTA だった場合は COM の仕様的にマーシャリングが必要無いわけですから、結局のところ 「RCW はスレッドのことなんてまったく何も気にしていない」 ということになるかと思います。
なるほど、だから #2 の方は両スレッドが MTA の場合についてのみ論じれば十分なわけですね。
それ以外の場合はそもそも COM の仕様的に不正な呼び出しになってしまうため、ReleaseComObject がどうこうという以前の問題になってしまうわけです。
なんか歯切れが悪いような気がするなぁ…>Dave の追加回答
返信削除