ページ

2009年7月2日木曜日

[.NET][COM] Marshal.ReleaseComObject の危険性について

先日書いた 「[IE][C#][COM] IE のセキュリティゾーンをプログラムから操作する」 に、元記事を書かれた Dennis "D.C." Dietrich さんがコメントをくれました。
(わざわざ翻訳して読んでくれたそうです。コメントをもらったのは 6/26 ですが、今まで書く暇がありませんでした)

そのコメントにて
Discussion of Marshal.ReleaseComObject and its dangers
こちらの記事を紹介してくれました。

[厳密な訳ではありません。かなり大雑把ですし、一部英文の意味がよくわからないところもあります。正確なところはぜひ原文をご覧ください]

Marshal.ReleaseComObject を使えば望んだときに即座にリリースできる。しかし、COM コンポーネントのマネージド表現である RCW のこれを呼ぶとき、もしこの RCW を AppDomain 上の他のマネージドコードが保持していると、リリースできなくて InvalidComObjectException が発生するだろう。

さらに悪いことは、呼び出しがすでにリリースされた RCW に行われると、その振る舞いは未定義である。AV [訳注: アクセスバイオレーションのこと?] が発生する機会になったり、メモリを腐らせたり、妙なクラッシュをするまでだらだらと実行し続けたりとデバッグをするのが非常に大変になったりする。

このリスクは CoCreateInstance が毎回同じインターフェースポインタを返すようなシングルトンな COM コンポーネントの場合にさらに悪化する。AppDomain 内の別々の独立したマネージドコードが同じシングルトンな COM コンポーネントの RCW を使っているとき、それらのうちのどれかがその COM コンポーネントの ReleaseComObject を呼び出すと他のところが壊れる。

これが、本当に必要だという場合を除いて ReleaseComObject を使うべきではないと私が強く勧める理由だ!

別の話として、本当に ReleaseComObject を使う必要があるときには、.NET Framework 2.0 からある Marshal.FinalReleaseComObject を使うべき。この API は RCW が何回参照されていても COM コンポーネントをリリースしてくれる。FinalReleaseComObject ならばループの中で ReleaseComObject がゼロを返すまで繰り返すなんてことをしなくて済む。

なるほど、ものすごく納得です。

IInternetSecuriryManager のような COM インターフェースの場合は、うかつに Marshal.ReleaseComObject を呼び出すくらいなら放置しておいた方が安全というですね。
ただ、リリースのタイミングで後始末するようなインターフェースもあったりするので、常に放置すればいいとは限りませんし、単純に 「○○すればいい」 というようなルール化は難しいでしょうからケースバイケースで判断するしかないでしょうね。

ちなみに、Excel を Automation 経由で呼び出す場合なんかの話は、上の議論とは別だと思います。
こういった場合は、Excel のプロセスをきちんと終了させるために、かなり気を使って Marshal.ReleaseComObject を適切に呼び出してやらなくちゃいけません。
ReleaseComObject を適切に呼び出すということは、言いかえれば生成からリリースまでそのすべてを自分で管理するということです。
ですから、上記で言っているような 「他のところでリリースしちゃった」 というようなことはおこり得ません。(と言うか、そういったことが無いように全部自分で管理するわけですが)


1 件のコメント:

  1. > Marshal.FinalReleaseComObject を使うべき。
     「呼び出しがすでにリリースされた RCW に行われると、その振る舞いは未定義である。」の問題をクリアしているわけではないですよね。
    http://msdn.microsoft.com/ja-jp/library/8bwh56xe.aspx
     ここにある図でいうと、ReleaseComObject が返す(デクリメントする)「参照回数」というのは、RCW の右(マネージ側)にある参照回数なのでしょうか。それとも左(アンマネージ側)にある参照回数なのでしょうか。
    Marshal.ReleaseComObject の、戻り値の説明には、

    ランタイム呼び出し可能ラッパーは、ラップされた COM オブジェクトへの参照を、それを呼び出しているマネージ クライアントの数に関係なく、1 つしか保持しないためです。

    と、書かれています。それでありながら、解説には

    同じ COM インターフェイスがアンマネージ コードからマネージ コードに複数回渡された場合、ラッパーの参照カウントは毎回インクリメントされ、ReleaseComObject を呼び出すと、残りの参照の数が返されます。

    と書かれています。同じ事が書かれていると思えないです。
     また、「ランタイム呼び出し可能ラッパー」の図では、複数の .NET クライアントが1回ずつ参照しているように見えます。1つの .NET クライアントが複数回参照した場合、参照回数はどのようになるのでしょう?
     でも、わかんないなぁ。。。参照すると、参照カウンタが上がりますよね。ReleaseComObject は、「参照カウンタを1減じて、0になったときに COM をリリースする」のですよね。そうであるなら、2つのオブジェクトが参照しているなら、参照カウンタは2になっており、片方が ReleaseComObject をしても、参照カウンタは1になるのであって0ではないので、解放は起こらないのでは?もちろん、参照カウンタへのアクセスがシリアル化されている事が前提ですが。
     ということを質問した人がいるみたい。 http://blogs.msdn.com/yvesdolc/archive/2004/07/21/190691.aspx ここの1中で、But then someone asked です。
    ��.リリースしたら、そのオブジェクトは使わないはずだから、別にかまわないよね。
    ��.複数のスレッドで参照しても、参照カウンタで管理しているはずだよね。
    それに対する Dave の回答が、よくわからない。
    ��.RCW が呼び出しを最適化して、その最適化された呼び出しの最下層にある参照をリリースすると、AV が発生する。
    ��.両方のスレッドが MTA なら、リリースの必要はまったくない。だから、MTA スレッドから ReleaseComObject を呼び出すと、AV やメモリ不正が発生する確率を高める。
    ��はともかく、2は、え~???

    返信削除