C# Tips
−COMのReleaseと_NewEnum−


[トップ] [目次]

このページの内容

C#からVisual SourceSafe 6.0a(6.0cも同様)を操作しようとして、_NewEnumの扱い方にちょっと、はまったので記録として書いときます。


VB6からVSSを呼び出す場合

VSSでは各ファイルをIVSSItemで表現しています。 これのVersionsプロパティを使うと、そのファイルの履歴(IVSSVersion)が取り出せます。 ただ、Versionsプロパティは直接IVSSVersionを返すわけではなく、IVSSVersionsを返します。 IVSSVersionsは、要するにIVSSVersionのコレクションで、中身は_NewEnumメソッドだけです。
IDLは下記のような感じです。


interface IVSSItem : IDispatch {
    // ...省略...
    [id(0x00000017), propget] HRESULT Versions([in, optional, defaultvalue(0)] long iFlags, [out, retval] IVSSVersions** pIVersions);
    // ...省略...
};

interface IVSSVersions : IDispatch {
    [id(0xfffffffc), restricted, hidden] HRESULT _NewEnum([out, retval] IUnknown** ppIEnum);
};

interface IVSSVersion : IDispatch {
    // ...省略...
};
IVSSVersions関係のIDL

VB6のコードはこんな感じになります。


Dim Version As IVSSVersion
Set Item = XXX                          ' IVSSItemを取得
For Each Version In Item.Versions(0)    ' 引数のゼロは単なるフラグ(今回は無関係)
    Version.Date                        ' 各バージョンにアクセスできる(履歴の取り出し)
Next
VB6でIVSSVersionsを扱う

IVSSItem.Versionsプロパティが返すIVSSVersionsに_NewEnumがあるからFor Each で回せるわけです。


C#から呼び出す場合

C#でも


tlbimp "c:\Program Files\VisualStudio\VSS\win32\SSAPI.DLL"

という感じでSSAPI.DLLからラッパーを作れば簡単に呼び出せます。
C#のコードはこんな感じになります。


IVSSItem Item = XXX;                    // IVSSItemを取得
foreach (IVSSVersion Version in Item.get_Versions(0)) {
    Version.Date;                       // 各バージョンにアクセスできる(履歴の取り出し)
}
C#でIVSSVersionsを扱う

簡単ですね。
ところがです。複数のIVSSItemを操作しようと


IVSSItem Item1 = XXX;                   // IVSSItemを取得
foreach (IVSSVersion Version in Item1.get_Versions(0)) {
    Version.Date;                       // 各バージョンにアクセスできる(履歴の取り出し)
}

IVSSItem Item2 = YYY;                   // IVSSItemを取得
foreach (IVSSVersion Version in Item2.get_Versions(0)) {
    Version.Date;
}
C#で複数のIVSSVersionsを扱う

こんな風にすると、2回目のget_Versions()で「System.Runtime.InteropServices.COMException (0x8004D75C): 履歴の操作が既に実行されています。」という例外が出てしまいます。
いろいろ試してみると、どうやら、IVSSVersion._NewEnumが返すIUnknownをRelease()する前に、再び_NewEnumを呼び出すとこの例外が発生するようです。
ところが、C#ではガーベジコレクタ(GC)によってオブジェクトが解放されるまではRelease()は呼ばれません。 そのために例外が出ちゃうようです。


強制的にCOMをRelease()する

ただ、やはりCOMが相手のときはGC任せばかりにはしていられないので、ちゃんとSystem.Runtime.InteropServices.MarshalクラスにReleaseComObjectメソッドが用意されています。 このメソッドを使えば、


IVSSItem Item = XXX;                    // IVSSItemを取得
// ...Itemを使う...
Marshal.ReleaseComObject(Item);

こんな風に、任意の時点で(GC任せにせず)COMをReleaseできます。

ちなみに、参照をなくしておいて(上記の例だとItem = nullとしておいて) GC.Collectメソッド、GC.WaitForPendingFinalizersメソッド、Marshal.ReleaseThreadCacheメソッドなんかを呼び出してみましたが、効果はありませんでした。 誰からも参照されない状態でGCが動けば、オブジェクトが破棄されてReleaseされても良さそうに思ったんですが...(自分が参照してないつもりでも、マーシャラとかどっかで参照してるのかもしれません)


これで解決、と思いきや

とりあえず、どんな風にラップされているのかtlbimpでできたDLLをildasmで見てみると


public abstract class IVSSItem {
    // ...省略...
    public IVSSVersions get_Versions([in][opt] int32 iFlags);
    // ...省略...
}

public abstract class IVSSVersions : System.Collections.IEnumerable {
    public virtual abstract System.Collections.IEnumerator GetEnumerator();
}

public abstract class IVSSVersion {
    // ...省略...
}
IVSSVersions関係のラップクラス

こんな風になっています。
ということは、IVSSVersions.GetEnumeratorメソッドが返したIEnumeratorをMarshal.ReleaseComObjectすればいいってことになります。
ところが、


IVSSItem Item = XXX;                    // IVSSItemを取得
IEnumerator e = Item.get_Versions(0).GetEnumerator();
Marshal.ReleaseComObject(e);

としてもMarshal.ReleaseComObjectのところで「System.ArgumentException: オブジェクトの型は __ComObject か、または __ComObject から派生しなければなりません。」という例外が出てしまいます。 __ComObjectは、ildasmで探したところ、mscorlib.dll内にあるprivateなクラスです(リファレンスには載っていません)。 privateなので直接キャストするわけにもいきません(キャストできるんだったら、そもそもこんな例外出ないんですが)。

ところで、IVSSVersions.GetEnumeratorメソッドが返すオブジェクトは何者なんでしょうか? GetEnumeratorメソッド自体の戻り値はIEnumeratorですが、IEnumeratorはインターフェースなので、このインターフェースを実装したクラスがあるはずです。 COMの_NewEnumはIUnknownを返しますが、QueryInterfaceしてみるとIEnumVARIANTです。 きっとIEnumVARIANTをラップしたクラスがどこかにあり、それのIEnumeratorインターフェースを返してるはずです。


IEnumerator e = Item.get_Versions(0).GetEnumerator();
Console.WriteLine(e.GetType().Name);

こうすれば簡単にクラス名を確認できます。
実際に試してみると、System.Runtime.InteropServices.CustomMarshalers.EnumeratorViewOfEnumVariantクラスでした。 このクラスはCustomMarshalers.dll内にあるprivateなクラスです(リファレンスには載っていません)。 ildasmで確認するとSystem.Runtime.InteropServices.ICustomAdapterインターフェースとSystem.Runtime.InteropServices.UCOMIEnumVARIANTインターフェースを実装しています。 EnumeratorViewOfEnumVariantクラス自体はprivateなのでどうにもなりませんが、これらのインターフェースはpublicですし、リファレンスにも載っています。 リファレンスを見てみるとICustomAdapter.GetUnderlyingObjectメソッドが使えそうです。


IEnumerator e = Item.get_Versions(0).GetEnumerator();
Console.WriteLine(((ICustomAdapter)e).GetUnderlyingObject().GetType().Name);

こうして確認したところ、ICustomAdapter.GetUnderlyingObjectメソッドが返すobjectは__ComObjectクラスであることがわかりました。
となれば、


IEnumerator e = Item.get_Versions(0).GetEnumerator();
Marshal.ReleaseComObject(((ICustomAdapter)e).GetUnderlyingObject());

こうしてやれば、めでたく、_NewEnumが返すIUnknownをReleaseすることができました。


[トップ] [目次]

株式会社ディーバ 青柳 臣一
2002/04/16