Visual Studio デベロッパー センター > Visual Studio フォーラム > Visual Studio ドキュメント フィードバック > 「Marshal.ReleaseComObject メソッド」の説明が矛盾しているように思います。
質問する質問する
 

全般的な情報交換「Marshal.ReleaseComObject メソッド」の説明が矛盾しているように思います。

  • 2009年7月7日 12:41JittaMVPユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダル
     

    Marshal.ReleaseComObject メソッド」 の説明についてです。

    こちらの、「戻り値」には、次のように書かれています。

    o に関連付けられているランタイム呼び出し可能ラッパーの参照カウントの新しい値。この値は通常 0 となります。ランタイム呼び出し可能ラッパーは、ラップされた COM オブジェクトへの参照を、それを呼び出しているマネージ クライアントの数に関係なく、1 つしか保持しないためです。

    ところが、「解説」には、次のように書かれています。

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

    一方には“呼び出している数に関係なく、1つしか保持しない”と書かれており、もう一方には“複数回渡された場合、ラッパーの参照カウントは毎回インクリメントされ(る)”と書かれています。

    この2つの説明は矛盾しているように思われます。分かり易い表現に修正して下さるよう、お願いします。


    なお、英語による表記も、次のように、日本語と同じ事が書かれていると思われます。

    Return Value

    This value is typically zero since the runtime callable wrapper keeps just one reference to the wrapped COM object regardless of the number of managed clients calling it.


    Remarks

    If the same COM interface is passed more than once from unmanaged to managed code, the reference count on the wrapper is incremented every time and calling ReleaseComObject returns the number of remaining references.



    ランタイム呼び出し可能ラッパー」の項を見ると、「COM オブジェクト←RCW」と、「RCW←.NET クライアント」の2つの参照があるようです。「戻り値」に書いてある「1つか保持しない」のは「COM オブジェクト←RCW」の参照のことではないでしょうか。RelaseComObject が返すべきなのは、「RCW←.NET クライアント」の参照数ではないでしょうか。

    また、ここから先は実装の問題になってきますが、複数の .NET クライアントから参照されたときに、同じ参照を返すのであれば、各 .NET クライアントが「自分は何回参照したか」を保持しなければならないと思います。これは、どこかに注記しておいていただきたいと思います。


    ところで、この投稿をするきっかけは、青柳さんのブログへのコメントで紹介された、「Discussion of Marshal.ReleaseComObject and its dangers」と、「More on ReleaseComObject (and why we did not implement IDisposable on the classes contained in the RCW)」の記事を読んだことです。「Discussion ...」の方に、「不用意に Marshal.ReleaseComObject を呼び出すと、他のスレッドで参照しているかもしれない参照を解放してしまう」と書かれています。それに対して「More ...」の方で、次のような質問があったと書かれています。「理解できない。参照するたびに参照カウントをインクリメントし、ReleaseComObject をするたびに減らすなら、他のスレッドが使っていても安全じゃないか。」私はこの質問と同じように思うのですが、返答が、よくわかりません。「両方のスレッドが MTA なら、そもそも管理する必要はない。それを ReleaseComObject を呼び出せば、アクセス違反(AV)を起こす。」と書かれているように思われます。もし、そう書かれているなら、ドキュメントにもそのことを反映していただきたいと思います。


    いろいろな COM 実装によって異なるのかもしれませんが、ドキュメントは正確に用意していただきたいと思います。


    Jitta@わんくま同盟

すべての返信

  • 2009年7月7日 23:44渋木宏明MVPユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダル
     

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

    は、RCW が COM 参照カウンタを「1個しか持たない」という「個数」の話で

    >ラッパーの参照カウントは毎回インクリメントされ、ReleaseComObject を呼び出すと、残りの参照の数が返されます。

    は、RCW が COM の参照カウンタを「複数回インクリメントする」という「回数」の話なので、特に矛盾は無いんじゃないでしょーか?

    >「自分は何回参照したか」を保持しなければならないと思います。

    必ずしもそうではないんですけどねぇ。

    C++ などで COM クライアントを実装する場合、スコープ単位で参照カウンタを管理することがほとんどです。
    なので、参照カウンタの値を直接意識する必要性はありません。

    ただ、.NET の場合は RCW によって COM 参照が隠ぺいされるので、その辺が扱いにくいわけですが…

  • 2009年7月8日 11:29Shinichi Aoyagi ユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダル
     

    まず、リファレンスの記述について。
    「戻り値」 の 「それを呼び出しているマネージ クライアントの数に関係なく、1 つしか保持しない」 というのは、たとえば、
     var rcw1 = (IHoge)Activator.CreateInstance( . . . );
     var rcw2 = rcw1;
    というように、マネージドコードでいくつ RCW を参照していても RCW 内の COM オブジェクトは一つだけであるということではないでしょうか?
    まぁ、rcw2 = rcw1 のような代入が行われたことを RCW にはわかりようがありませんから当然と言えば当然ですが。

    次に 「解説」 の方です。
    まず、「同じ COM インターフェイスがアンマネージ コードからマネージ コードに複数回渡された場合」 という部分の 「同じ COM インターフェース」 とは何なのか?ですが、これは具体的にはポインタ値が同じものということではないかと思います。
    COM インターフェースを取得する方法は、
     ・Activator.CreateInstance() などで COM オブジェクトを生成する。
     ・メソッドの引数や戻り値などが COM インターフェースを返す。(たとえば HRESULT GetHoge([out, retval] IHoge** ppHoge) といったメソッド)
    などがありますが、これらで取得した COM インターフェースのポインタ値と同じポインタをすでに持っている RCW があった場合はその RCW が使い回されるということではないかと思います。
    この場合に、RCW は何回同じポインタ値を参照したのかをカウントしているということでしょう。
    そして、このカウンタが 「解説」 で言っている 「ラッパーの参照カウントは毎回インクリメントされ」 ということだと思います。
    Marshal.ReleaseComObject() はこのカウンタを戻り値とし、Marshal.FinalReleaseComObject() はこのカウンタの回数だけ COM インターフェースの Release() を呼んでくれる、また、RCW が GC によって解放されるときにはこのカウンタの数だけ Release() を呼び出してくれる、ということです。

    私はこのように思ったのですが、こういうことであれば書かれていることは正しいと言っていいと思います。

    ランタイム呼び出し可能ラッパー」の項を見ると、「COM オブジェクト←RCW」と、「RCW←.NET クライアント」の2つの参照があるようです。「戻り値」に書いてある「1つか保持しない」のは「COM オブジェクト←RCW」の参照のことではないでしょうか。RelaseComObject が返すべきなのは、「RCW←.NET クライアント」の参照数ではないでしょうか。

    RCW の実態は単なるクラスではないでしょうか?
    たとえば、 How to: Create Wrappers Manually には手動で RCW を作る例が載っています。
    これの最後のところに出来上がった RCW のソースコードが載っていますが、見ての通りで、COM とのやり取りはいろいろな属性を元にランタイムが面倒を見てくれるようですが、RCW 自体は普通のクラスです。
    また、「ランタイム呼び出し可能ラッパー」 の図を見ても 「RCW←.NET クライアント」 はマネージドな世界の参照として描かれています。
    あるクラスのインスタンスが誰にどれだけ参照されているかをインスタンス側で知ることは普通はできませんから、『「RCW←.NET クライアント」の参照数』 を RCW 側で把握することはできないのではないでしょうか?


    青柳 臣一 (Shinichi Aoyagi)
  • 2009年7月9日 12:34JittaMVPユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダル
     

    渋木宏明さん、ありがとうございます。

    C++ などで COM クライアントを実装する場合、スコープ単位で参照カウンタを管理することがほとんどです。
    なので、参照カウンタの値を直接意識する必要性はありません。

    ReleaseComObject の戻り値は、「o に関連付けられているランタイム呼び出し可能ラッパーの参照カウントの新しい値。」と書かれているわけですが、この「参照カウントの新しい値」というのは、参照カウンタをインクリメントした回数ではなく、保持している参照カウンタの数ということでしょうか。すると、ReleaseComObject は、参照カウンタをデクリメントし、それが0になると参照(カウンタ)を解放する。その後、保持している参照カウンタがいくつあるかを返す?


    Shinichi Aoyagiさん、ありがとうございます。

    私はこのように思ったのですが、こういうことであれば書かれていることは正しいと言っていいと思います。

    「参照カウンタ」という語を使いましたが、MSDN には「参照カウント」という語が使われていました。カウンターの数と、そのカウンターが指している数字、2つのことを言っているというのであれば、書いてあることは理解できます。が、その違いがわかりにくいと思います。いや、COM を知っている方々にとっては当たり前のことなのかもしれませんけど。


    Jitta@わんくま同盟
  • 2009年7月10日 1:09渋木宏明MVPユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダル
     

    >この「参照カウントの新しい値」というのは、参照カウンタをインクリメントした回数ではなく、保持している参照カウンタの数ということでしょうか。

    いいえ。文字どおり「(COM オブジェクトのインスタンスの)参照カウンタが保持している値」です。

    「参照カウンタ」という語を使いましたが、MSDN には「参照カウント」という語が使われていました。カウンターの数と、そのカウンターが指している数字、2つのことを言っているというのであれば、書いてあることは理解できます。が、その違いがわかりにくいと思います。

     

  • 2009年7月10日 10:44Shinichi Aoyagi ユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダル
     
    例えば,
    IUnknownインタフェースから IHogeClassインタフェースを取得するのに
    QueryInterface を呼び出して取得します。
    そのときに,COM側でAddRefを呼び出してから,
    IHogeClassインタフェースは,こちらにやって来ます。

    本来なら,まとめて管理してるんだったら,
    上記の参照カウンタの値をインクリメントしないといけないですよね?
    でも,実際は,そうはなっていないようです。

    いや、実際にそうなっていないとなると、MSDN に 「同じ COM インターフェイスがアンマネージ コードからマネージ コードに複数回渡された場合、ラッパーの参照カウントは毎回インクリメントされ」 とあるのがまるっきり嘘ということになっちゃいます。さすがにそれは無いでしょう。
    個人的な感触でも COM インターフェースの参照数はちゃんと管理されているように思います。
    (わざわざ検証したわけではありませんが、COM インターフェースを参照した回数だけ ReleaseComObject してやればちゃんとその COM インターフェースは解放されていると思います)

    ですから、

    ということは,結局,
    上記の参照カウンタは,あくまで参考なんでしょう。
    なんてことは無いと思います。
    青柳 臣一 (Shinichi Aoyagi)
  • 2009年7月10日 10:46yayadon ユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダル
     
    あるクラスのインスタンスが誰にどれだけ参照されているかをインスタンス側で知ることは普通はできませんから、『「RCW←.NET クライアント」の参照数』 を RCW 側で把握することはできないのではないでしょうか?

    その通りで,
    managed code <----> managed code 間で
    やりとりする分には,
    COM <-- RCW の参照カウントは 1 のままにしておくと決めたんでしょう。

    インクリメントしても,
    最初に代入した変数はまだ把握しようがありますが,
    クラスのメンバにインタフェースポインタを渡したとして
    クラスの内部の別プロパティから他の変数やプロパティに渡ったとしても,
    インクリメントを示すコード上の表記がないので,
    インクリメントのしようがないですよね。

    だから,
    ReleaseComObject すると戻ってくる値は,

    typcally 0

    ということです。絶対に 0 とは書いてあるわけではなく,
    ふつうは ,0 だという意味だから,問題ないですよね。



    Remarks では,注意事項が書かれている場合が多々あって,
    今回のも注意事項で,
    同じインタフェースポインタの受け渡しが
    再度以下のような受け渡しで起きる(re-entry)

    unmanaed code ----> managed code

    場合は,インクリメントが起きるとの注意喚起だと思います。

    managed --> managed では
    そもそも把握できないので,
    インクリメントしなかったけれども,
    こちらは把握できるのでインクリメントしているんでしょう。

    ヘルプには書いてあるかどうかわからないけど,
    RCWは,
    IUnknown や IDispatch や
    接続ポイント(イベントのこと)となる IConnectionPoint や
    エラーがらみ等のインタフェースを,まとめて管理してますよね。

    managed側で実際に使用されている数はわからないのに
    上記の参照カウンタだけで
    実際のCOMインタフェースポインタをリリースするタイミングを決めないといけません。

    実際の実装が,どういうタイミングになっているのかは,わかりません。
    でも,
    上記の参照カウンタだけで,
    そもそも正確なタイミングを決めれるんでしょうか?

    例えば,
    IUnknownインタフェースから IHogeClassインタフェースを取得するのに
    QueryInterface を呼び出して取得します。
    そのときに,COM側でAddRefを呼び出してから,
    IHogeClassインタフェースは,こちらにやって来ます。

    本来なら,まとめて管理してるんだったら,
    上記の参照カウンタの値をインクリメントしないといけないですよね?
    でも,実際は,そうはなっていないようです。

    ということは,
    ReleaseComObject が呼ばれて,
    参照カウントの値が,0 になっていなくても,
    どのインタフェースが利用されているのかわかりようがありません。

    これを回避するには,
    RCW内部で,内部用の参照カウンタを持たせて,
    QueryInterfaceごとに,インクリメントしていかないといけません。

    ここでパフォーマンスの問題が出てきます。
    参照カウントのインクリメントは,
    STAスレッドの時は,ふつうに加算・減算でいいんですが,
    MTAの場合は,InterlockedIncrement と InterlockedDecrement を
    使って,加算・減算する必要があります。

    これがパフォーマンスが悪いです。
    なので,
    例えば,ATL では,STA の時と MTA の時で
    参照カウントの実装が変わるようにできてます。
    (STAの時だけ緩和されるだけですが)


    で,
    このパフォーマンスの悪さというと,私が思い浮かべるのは,
    Visual Basic チームから来た開発者が,
    .NET Framework の開発スタート時に,
    参照カウントの仕組みが必要だと主張したんですが,
    その主張が退けられた理由のひとつになったそうです。
    (正確には,他にも相互参照の問題等もあったのですが,この問題もあった)

    で,
    RCWでは,どうしたかというと,
    ATLのようにやる方法もあったんでしょうけれど,
    その方法でも効果があるのはSTAの時だけなので,
    推測ですが,
    いちいち個別にカウントすることを止めたんでしょう。
    実際はどうなってるのか不明ですが。
    ( "x86での最適化ため" というのはこのことでしょう。
    x86 では atom operation できないので)


    ということは,結局,
    上記の参照カウンタは,あくまで参考 なんでしょう。
    なので,
    1回でも ReleaseComObject が呼ばれると,
    PCW側は,
    全インタフェースポインタの参照カウント値を把握してるわけではないので,
    解放のタイミングの条件がややこしくなってきますよね?
    なので,
    その後の利用で,判断が付かないときはAV にする場合があるんじゃないですかね。


    COM側は参照カウンタが必要ですが,
    利用側は参照カウンタは本来は必要ありません。

    RCWが参照カウンタを持っているのは,
    .NETコードに対して
    「まだ,COMを使用してます」 ってことを示す目的で
    作ってあるだけなんでしょう。


    # あくまで推測です。
     


    稍丼 / yayadon
  • 2009年7月10日 10:51yayadon ユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダル
     
    例えば,
    IUnknownインタフェースから IHogeClassインタフェースを取得するのに
    QueryInterface を呼び出して取得します。
    そのときに,COM側でAddRefを呼び出してから,
    IHogeClassインタフェースは,こちらにやって来ます。

    本来なら,まとめて管理してるんだったら,
    上記の参照カウンタの値をインクリメントしないといけないですよね?
    でも,実際は,そうはなっていないようです。

    いや、実際にそうなっていないとなると、MSDN に 「同じ COM インターフェイスがアンマネージ コードからマネージ コードに複数回渡された場合、ラッパーの参照カウントは毎回インクリメントされ」 とあるのがまるっきり嘘ということになっちゃいます。さすがにそれは無いでしょう。
    個人的な感触でも COM インターフェースの参照数はちゃんと管理されているように思います。
    (わざわざ検証したわけではありませんが、COM インターフェースを参照した回数だけ ReleaseComObject してやればちゃんとその COM インターフェースは解放されていると思います)

    ですから、

    ということは,結局,
    上記の参照カウンタは,あくまで参考 なんでしょう。
    なんてことは無いと思います。
    青柳 臣一 (Shinichi Aoyagi)


    いやいや

    COM側は参照カウンタが必要ですが,
    利用側は参照カウンタは本来は必要ない

    ですよね?


    # すいません,あまりに書き直ししたので削除&追加しようとしたらレスを入れられてしまいました。
    稍丼 / yayadon
  • 2009年7月10日 11:18yayadon ユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダル
     
    # 確か,見たことがあったので,探してみました。

    ヘルプの

    http://msdn.microsoft.com/en-us/library/8023ct8s.aspx

    を見てください。

    RCW は,INew インタフェースを管理してます。

    でも,
    図で見ると,わかるように,

    IUnknown
    IDispatch
    IErrorInfo
    IProvideClassInfo

    も同時に管理してますよね。

    これらインタフェースを使った時に
    参照カウンタの値は上げ/下げしてないですよね。



    追記:
    ヘルプの言ってることは,上の例の場合だと,
    INewを再エントリすると参照カウンタが上がるって説明なので
    矛盾してないですよね。
    • 編集済みyayadon 2009年7月10日 11:26追記
    • 編集済みyayadon 2009年7月10日 11:21リンク修正
    •  
  • 2009年7月11日 19:15yayadon ユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダル
     
    Yves Dolceさんの発言の
    どうしても ReleaseComObject を呼んで解放したい場合は,
    FinalReleaseComObject を呼ぶか,
    ReleaseComObject の戻り値が 0 になるまでループしてくれ
    とあるのは,以下の理由だと思われる。

    RCW の参照カウントが 1 の時は,
    ReleaseComObject が呼ばれた時,
    INew をリリースするのに加えて,
    現在,こっそり使っているインタフェースを
    すべて解放すればOK。問題なし。

    RCW の参照カウントが 2 以上の時は,
    INew 自体は,リリースを1回する。
    参照カウントは ひとつ減らせばOK。2 なら 1 となる。
    ただ,
    現在こっそり使っているインタフェースは
    どれを解放すればいいのか?
    それとも横着して全部残しておいていいのか?
    RCWは,それを判断できない。

    なので,
    やるんなら,0 になるまで一気にやってくれってことでしょう。
    Yves Dolceさんが言われていることは理解できます。
    かつ,ヘルプと矛盾してないですし。

    ICallFactoryもメソッド呼び出しの前に聞いてくるので
    ラップしたインタフェースと同一インスタンス絡みのものには
    見かけ上の参照カウントには(直接は)関係なく,
    ひとつのRCWが,面倒見てくれているものがあるんでしょう。
    そのため参照カウントが 2以上にいったん上がると話をややこしくなる。
    稍丼 / yayadon
    • 編集済みyayadon 2009年7月11日 19:30
    •  
  • 2009年7月14日 9:19yayadon ユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダルユーザーのメダル
     
    # COM側に仕掛けを入れて,しっかり調べてみました。


    RCW が 最初に CreateInstanceする時には
    riid には __uuifof(IUnknown) を指定してインスタンスを作成してきます。
    型がどうであれ,IUnknownインタフェースを使って います。
    なので,
    タイプライブラリで default指定したインタフェースは最初には使っていません。
    この時に,RCW の参照カウントは 1 に上がって います。
    また,
    代入先変数の型が特定のインタフェースの型であったとしても
    その地点では,QueryInterfaceしてきません。

    で,
    メソッドを使う前に,初めて,そのメソッドが属するインタフェースを
    QueryInterface してきます。(二度目以降はしてこない)
    この時には,RCW の参照カウントは上がりません。

    変数自体をキャストした時も,その地点では,QueryInterfaceしてきません。
    メソッドを使う前に,初めて,そのメソッドが属するインタフェースを
    QueryInterface してきます。(二度目以降はしてこない)
    この時には,RCW の参照カウントは上がりません。

    基本的には 1 のままです。
    COM側の参照カウントの増減とは無関係です。


    逆に上がる場合:

    COM側のメソッドやプロパティの戻り値(or outパラメータ) では,
    その型や受ける変数の型に関係なく,
    受け取ったインタフェースに対して,
    IUnknownインタフェースを QueryInterface してき
    ます。

    理由は,
    IUnknownインタフェースが 同じ値であれば,
    同一インスタンスからのものであると確認できるためでしょう。
    なので,値が同じだった時に
    RCW の参照カウントは 1 上がる
    ことになります。

    (追記:
    値が別物の時は,RCW自体が別物 になるため,
    その RCW の参照カウントは 1 になることになる。)

    共有しているわけなので,
    そのインタフェースのメソッドの呼び出しは,以前に呼び出していれば,
    そのメソッドが属するインタフェースはQueryInterfaceしてきません。


    シングルトンCOMオブジェクト の new 時,
    RCW が CreateInstanceする時には
    riid には __uuifof(IUnknown) を指定してインスタンスを作成してきます。
    型がどうであれ,IUnknownインタフェースを使って います。
    また,
    IUnknownインタフェースが
    常に同一インスタンスからなので,
    RCW の参照カウントは 無関係な場所で new されても,
    常に 1 上がります。

    (※ 他にも増えるパターンがあるかもしれません)


    稍丼 / yayadon
    • 編集済みyayadon 2009年7月14日 9:37追記
    • 編集済みyayadon 2009年7月14日 14:01追記
    • 編集済みyayadon 2009年7月14日 10:42追記
    • 編集済みyayadon 2009年7月14日 9:39
    • 編集済みyayadon 2009年7月14日 9:40修正
    •