次の方法で共有


基本的な本能

リフレクションを使用して COM オブジェクトを検査する

Lucian Wischik

コードは MSDN コード ギャラリーからダウンロードできます。
オンラインでのコードの参照

目次

タイプ ライブラリとランタイム呼び出し可能ラッパー
RCW を持たない型の場合
ITypeInfo を使用する
型参照を追跡する
メンバを取得する
プリミティブ型と合成型
値の COM 表現
COM オブジェクトのプロパティをダンプする
IDispatch.Invoke を使用する
説明

たいていの開発者は、操作する COM を取得しようとするときにストレスを感じます。これが成功したときは幸せを感じます。オブジェクトの動作を理解する一般的なコツは、Microsoft .NET Framework のリフレクション機能を使用して調査を行うことです。.NET リフレクションは、COM オブジェクトで動作する場合もあります。これを理解するには、次のコードを見てください。このコードでは、.NET リフレクションを使用してオブジェクトのメンバのリストを取得し、表示します。

Dim b As New SpeechLib.SpVoice
Console.WriteLine("GETTYPE {0}", b.GetType())
For Each member In b.GetType().GetMembers()
    Console.WriteLine(member)
Next

コンソールに、次の出力が生成されます。

GETTYPE SpeechLib.SpVoiceClass
Void Speak(System.String, UInt32, UInt32 ByRef)
Void SetVoice(SpeechLib.ISpObjectToken)
Void GetVoice(SpeechLib.ISpObjectToken ByRef)
Int32 Volume
...

しかし、このコードは、すべての COM オブジェクトで動作するわけではありません。そこで、COM リフレクションを使用する必要があります。このコラムでは、この理由と手順について説明します。

オブジェクトに対してリフレクションを使用する必要があるのはなぜでしょうか。筆者は、リフレクションがデバッグおよびログ記録に役立つことを発見しました。リフレクションを使用すると、オブジェクトで利用可能なすべての情報を出力する汎用的な "ダンプ" ルーチンを作成できます。このコラムのコードは、独自の "ダンプ" ルーチンを作成するのに十分役立ちます。作成したルーチンは、デバッグ中にイミディエイト ウィンドウから起動することもできます。Visual Studio デバッガで提供される COM オブジェクトの情報は多くないため、これは非常に役立ちます。

実稼働環境で使用する場合は、プラグイン コンポーネントを組み込むアプリケーションを作成したときにリフレクションが役立ちます。コンポーネントはディレクトリに配置されるか、またはレジストリにリストされます。アプリケーションではこれらのコンポーネントを検査し、公開されるクラスおよびメソッドを検索する必要があります。たとえば、Visual Studio ではこの方法でリフレクションを使用して IntelliSense を組み込みます。

タイプ ライブラリとランタイム呼び出し可能ラッパー

では、例としてプロジェクトを構築してみましょう。まず、プロジェクトを作成します。[プロジェクト]、[参照の追加] の順に選択し、COM 参照を追加します。ここでは、"Microsoft Speech Object Library" SpeechLib を使用します。図 1 に、先に示したリフレクション コードの実行時に検査される、関連するエンティティおよびファイルを示します。

fig01.gif

図 1 SpeechLib のリフレクション

Sapi.dll は SpeechLib が含まれている DLL です。これは、%windir%\system32\speech\common\sapi.dll にあります。この DLL には、SpVoice COM クラスの実装と、このクラスのすべてのリフレクション情報を含む TypeLibrary の実装の両方が含まれています。TypeLibrary は省略できますが、システムのほぼすべての COM コンポーネントに該当するライブラリが存在します。

Visual Studio での [プロジェクト]、[参照の追加] の操作によって、Interop.SpeechLib.dll が自動的に生成されました。ジェネレータで TypeLibrary が反映され、SpVoice の相互運用型が生成されます。この型は、TypeLibrary で見つかったすべてのネイティブ COM メソッドに対応するマネージ メソッドが含まれているマネージ クラスです。Windows SDK から tlbimp.exe コマンド ライン ツールを使用して、自分で相互運用アセンブリを作成することもできます。相互運用型のインスタンスはランタイム呼び出し可能ラッパー (RCW) と呼ばれ、COM クラスのインスタンスへのポインタをラップします。

次の Visual Basic コマンドを実行すると、RCW (相互運用型のインスタンス) のほか、SpVoice COM クラスのインスタンスが作成されます。

Dim b As New SpeechLib.SpVoice

変数 "b" は RCW を参照します。このため、"b" のリフレクションは、実際は TypeLibrary から構築された同等のマネージ コードのリフレクションになります。

ConsoleApplication1.exe を展開するユーザーは Interop.SpeechLib.dll も展開する必要があります (ただし、Visual Studio 2010 では相互運用型を直接 ConsoleApplication1.exe 内でコピーできるようになります。これにより、展開が非常に簡素化されます。この機能は、"No-Primary-Interop-Assembly (プライマリ相互運用アセンブリなし)" または略して "No-PIA" と呼ばれます)。

RCW を持たない型の場合

COM オブジェクトの相互運用アセンブリがない場合はどうなるでしょうか。たとえば、CoCreateInstance を通じて COM オブジェクト自身を作成した場合や、よくあることですが、COM オブジェクトに対してメソッドを呼び出したときに事前に型がわからない COM オブジェクトが返された場合は、どうなるでしょうか。アンマネージ アプリケーションのマネージ プラグインを作成し、アプリケーションから COM オブジェクトが渡された場合はどうなるでしょうか。レジストリを調べて作成する COM オブジェクトを見つけた場合はどうなるでしょうか。

これらの場合はすべて、RCW へのオブジェクト参照ではなく、COM オブジェクトへの IntPtr 参照が提供されます。その IntPtr で RCW を求めた場合は、図 2 のようになります。

fig02.gif

図 2 ランタイム呼び出し可能ラッパーを取得する

図 2 では、CLR によって既定の RCW が提供されていることがわかります。これは、既定の相互運用型 "System.__ComObject" のインスタンスです。このリフレクションは、次のようになります。

Dim b = CoCreateInstance(CLSID_WebBrowser, _
                   Nothing, 1, IID_IUnknown)
Console.WriteLine("DUMP {0}", b.GetType())
For Each member In b.GetType().GetMembers()
    Console.WriteLine(member)
Next

役に立つメンバはありません。含まれているのは以下のメンバのみです。

DUMP System.__ComObject
System.Object GetLifetimeService()
System.Object InitializeLifetimeService()
System.Runtime.Remoting.ObjRef CreateObjRef(System.Type)
System.String ToString()
Boolean Equals(System.Object)
Int32 GetHashCode()
System.Type GetType()

このような COM オブジェクトで役に立つリフレクションを取得するには、自分で TypeLibrary のリフレクションを操作する必要があります。これを行うには、ITypeInfo を使用します。

しかし、最初に手短に注意しておきます。メソッドで Object、IDispatch、ITypeInfo、または他の .NET クラスまたはインターフェイスが返された場合は、RCW への参照が提供されます。解放は .NET で自動的に処理されます。しかし、メソッドで IntPtr が返された場合は、COM オブジェクト自身への参照が返されたことを意味します。そしてほとんどの場合は、Marshal.Release を呼び出す必要があります (これは、その IntPtr を返したメソッドの正確なセマンティクスに応じて異なります)。次のようになります。

   Dim com As IntPtr = ...
   Dim rcw = Marshal.GetObjectForIUnknown(com)
   Marshal.Release(com)

しかし、より一般的には関数をマーシャリングで宣言します。図 3 に示す CoCreateInstance の宣言のように、GetObjectForIUnknown および Release がマーシャラで自動的に呼び出されます。

図 3 CoCreateInstance

<DllImport("ole32.dll", ExactSpelling:=True, PreserveSig:=False)> _
Function CoCreateInstance( _
    ByRef clsid As Guid, _
    <MarshalAs(UnmanagedType.Interface)> ByVal punkOuter As Object, _
    ByVal context As Integer, _
    ByRef iid As Guid) _
    As <MarshalAs(UnmanagedType.Interface)> Object
End Function

Dim IID_NULL As Guid = New Guid("00000000-0000-0000-C000-000000000000")
Dim IID_IUnknown As Guid = New _
    Guid("00000000-0000-0000-C000-000000000046")
Dim CLSID_SpVoice As Guid = New _
    Guid("96749377-3391-11D2-9EE3-00C04F797396")

Dim b As Object = CoCreateInstance(CLSID_SpVoice, Nothing, 1, _ 
    IID_IUnknown)

ITypeInfo を使用する

ITypeInfo は COM クラスおよびインターフェイスの System.Type と同等です。これを使用して、クラスまたはインターフェイスのメンバを列挙できます。この例ではメンバを出力しますが、ITypeInfo を使用して実行時にメンバを参照し、IDispatch を介してそれらのメンバを起動したり、プロパティ値を取得したりできます。図 4 に、ITypeInfo および使用する必要のあるその他のすべての構造を示します。

fig04.gif

図 4 ITypeInfo および型情報

まずは、特定の COM オブジェクトの ITypeInfo を取得します。rcw.GetType() を使用できればよいのですが、これだと RCW 自身の System.Type 情報が返されます。組み込み関数 Marshal.GetITypeInfoForType(rcw) を使用できればよいのですが、残念なことに、これは相互運用アセンブリからの RCW でのみ動作します。そこで、ITypeInfo を手動で取得する必要があります。

次のコードは、RCW が mscorlib のスタブから来ているか、または適切な相互運用アセンブリから来ているかにかかわらず、両方のケースで動作します。

Dim idisp = CType(rcw, IDispatch)
Dim count As UInteger = 0
idisp.GetTypeInfoCount(count)
If count < 1 Then Throw New Exception("No type info")
Dim _typeinfo As IntPtr
idisp.GetTypeInfo(0, 0, _typeinfo)
If _typeinfo = IntPtr.Zero Then Throw New Exception("No ITypeInfo")
Dim typeInfo = CType(Marshal.GetTypedObjectForIUnknown(_typeinfo, _
                     GetType(ComTypes.ITypeInfo)), ComTypes.ITypeInfo)
Marshal.Release(_typeinfo)

このコードではインターフェイス IDispatch を使用しています。このインターフェイスは .NET Framework のどこにも定義されていないため、図 5 に示すように自分で定義する必要があります。今回の目的では必要ないため、関数 GetIDsOfNames は空のまま残しましたが、インターフェイスでは正しい数のメソッドを正しい順序でリストする必要があるため、このエントリは含める必要があります。

図 5 IDispatch インターフェイスを定義する

''' <summary>
''' IDispatch: this is a managed version of the IDispatch interface
''' </summary>
''' <remarks>We don't use GetIDsOfNames or Invoke, and so haven't 
''' bothered with correct signatures for them.</remarks>
<ComImport(), Guid("00020400-0000-0000-c000-000000000046"), _
 InterfaceType(ComInterfaceType.InterfaceIsIUnknown)> _
 Interface IDispatch
    Sub GetTypeInfoCount(ByRef pctinfo As UInteger)
    Sub GetTypeInfo(ByVal itinfo As UInteger, ByVal lcid _
      As UInteger, ByRef pptinfo As IntPtr)
    Sub stub_GetIDsOfNames()
    Sub Invoke(ByVal dispIdMember As Integer, ByRef riid As Guid, _
               ByVal lcid As UInteger, ByVal dwFlags As UShort, _
               ByRef pDispParams As ComTypes.DISPPARAMS, _
               ByRef pVarResult As [VARIANT], ByRef pExcepInfo As IntPtr, _
               ByRef pArgErr As UInteger)
End Interface

IDispatch の InterfaceType 属性がなぜ ComInterfaceType.InterfaceIsIDisapatch ではなく ComInterfaceType.InterfaceIsUnknown に設定されているかについて疑問に思われるかもしれません。これは、InterfaceType 属性が現在のインターフェイスではなく、インターフェイスの継承元を表すためです。

ITypeInfo は取得されました。これから読み取りを開始します。図 6 をご覧ください。型情報をダンプする関数を実装しています。GetDocumentation の第 1 パラメータは MEMBERID です。GetDocumentation は、型の各メンバに関する情報を返すようになっています。しかし、MEMBERID_NIL (値 -1) を渡して型自体の情報を取得することもできます。

図 6 DumpTypeInfo

''' <summary>
''' DumpType: prints information about an ITypeInfo type to the console
''' </summary>
''' <param name="typeInfo">the type to dump</param>
Sub DumpTypeInfo(ByVal typeInfo As ComTypes.ITypeInfo)

    ' Name:
    Dim typeName = "" : typeInfo.GetDocumentation(-1, typeName, "", 0, "")
    Console.WriteLine("TYPE {0}", typeName)

    ' TypeAttr: contains general information about the type
    Dim pTypeAttr As IntPtr : typeInfo.GetTypeAttr(pTypeAttr)
    Dim typeAttr = CType(Marshal.PtrToStructure(pTypeAttr, _
                         GetType(ComTypes.TYPEATTR)), ComTypes.TYPEATTR)
    typeInfo.ReleaseTypeAttr(pTypeAttr)
    ...

End Sub

マーシャリングの動作に注目してください。typeInfo.GetTypeAttr を呼び出すと、アンマネージ メモリ ブロックが割り当てられ、ポインタ pTypeAttr が返されます。次に、Marshal.PtrToStructure によってこのアンマネージ ブロックがマネージ ブロックにコピーされます (後でガベージ コレクションが実行されます)。そのため、typeInfo.ReleaseTypeAttr を直ちに呼び出すことができます。

前に示したように、typeAttr が存在しているメンバおよび実装されたインターフェイスの数を認識している必要があります (typeAttr.cFuncs、typeAttr.cVars、および typeAttr.cImplTypes)。

型参照を追跡する

最初に完了する必要のある作業は、実装または継承されたインターフェイスのリストを取得することです (COM では、別のクラスからの継承はありません)。このコードを次に示します。

' Inheritance:
For iImplType = 0 To typeAttr.cImplTypes - 1
    Dim href As Integer
    typeInfo.GetRefTypeOfImplType(iImplType, href)
    ' "href" is an index into the list of type descriptions within the
    ' type library.
    Dim implTypeInfo As ComTypes.ITypeInfo
    typeInfo.GetRefTypeInfo(href, implTypeInfo)
    ' And GetRefTypeInfo looks up the index to get an ITypeInfo for it.
    Dim implTypeName = ""
    implTypeInfo.GetDocumentation(-1, implTypeName, "", 0, "")
    Console.WriteLine("  Implements {0}", implTypeName)
Next

ここでは、間接指定が層になっています。GetRefTypeOfImplType からは実装された型の ITypeInfo は直接渡されません。代わりに、ITypeInfo へのハンドルが提供されます。そのハンドルを参照するのは関数 GetRefTypeInfo です。次に、使い慣れた GetDocumentation(-1) で実装された型の名前を取得します。ITypeInfo へのハンドルについては後でまた説明します。

メンバを取得する

各フィールド メンバのリフレクションでは、フィールドを記述する VARDESC が各フィールドにあります。ここでもまた、typeInfo オブジェクトによってアンマネージ メモリ ブロック pVarDesc が割り当てられます。これをマネージ ブロック varDesc にマーシャリングし、アンマネージ ブロックを解放します。

' Field members:
For iVar = 0 To typeAttr.cVars - 1
    Dim pVarDesc As IntPtr : typeInfo.GetVarDesc(iVar, pVarDesc)
    Dim varDesc = CType(Marshal.PtrToStructure(pVarDesc, _
                        GetType(ComTypes.VARDESC)), ComTypes.VARDESC)
    typeInfo.ReleaseVarDesc(pVarDesc)
    Dim names As String() = {""}
    typeInfo.GetNames(varDesc.memid, names, 1, 0)
    Dim varName = names(0)
    Console.WriteLine("  Dim {0} As {1}", varName, _
                      DumpTypeDesc(varDesc.elemdescVar.tdesc, typeInfo))
Next

"GetNames" は興味深い関数です。おそらくは、各メンバにいくつかの名前があります。最初の名前を取得するだけで十分です。

関数メンバに対するリフレクションのコードは、一般に同じです (図 7 を参照)。戻り値の型は funcDesc.elemdescFunc.tdesc です。仮引数の数は funcDesc.cParams で指定され、仮引数は配列 funcDesc.lprgelemdescParam に格納されます (マネージ コードからこのようなアンマネージ配列にアクセスすることは、ポインタ演算が必要となるため好ましくありません)。

図 7 関数メンバのリフレクション

For iFunc = 0 To typeAttr.cFuncs - 1

   ' retrieve FUNCDESC:
   Dim pFuncDesc As IntPtr : typeInfo.GetFuncDesc(iFunc, pFuncDesc)
   Dim funcDesc = CType(Marshal.PtrToStructure(pFuncDesc, _
                         GetType(ComTypes.FUNCDESC)), ComTypes.FUNCDESC)
   Dim names As String() = {""}
   typeInfo.GetNames(funcDesc.memid, names, 1, 0)
   Dim funcName = names(0)

   ' Function formal parameters:
   Dim cParams = funcDesc.cParams
   Dim s = ""
   For iParam = 0 To cParams - 1
        Dim elemDesc = CType(Marshal.PtrToStructure( _
                  New IntPtr(funcDesc.lprgelemdescParam.ToInt64 + _
                  Marshal.SizeOf(GetType(ComTypes.ELEMDESC)) * iParam), _
                  GetType(ComTypes.ELEMDESC)), ComTypes.ELEMDESC)
        If s.Length > 0 Then s &= ", "
        If (elemDesc.desc.paramdesc.wParamFlags And _
            Runtime.InteropServices.ComTypes.PARAMFLAG.PARAMFLAG_FOUT) _
             <> 0 Then s &= "out "
        s &= DumpTypeDesc(elemDesc.tdesc, typeInfo)
   Next

   ' And print out the rest of the function's information:
   Dim props = ""
   If (funcDesc.invkind And ComTypes.INVOKEKIND.INVOKE_PROPERTYGET) _
      <> 0 Then props &= "Get "
   If (funcDesc.invkind And ComTypes.INVOKEKIND.INVOKE_PROPERTYPUT) _
      <> 0 Then props &= "Set "
   If (funcDesc.invkind And ComTypes.INVOKEKIND.INVOKE_PROPERTYPUTREF) _
     <> 0 Then props &= "Set "
   Dim isSub = (FUNCDESC.elemdescFunc.tdesc.vt = VarEnum.VT_VOID)
   s = props & If(isSub, "Sub ", "Function ") & funcName & "(" & s & ")"
   s &= If(isSub, "", " as " & _
     DumpTypeDesc(funcDesc.elemdescFunc.tdesc, typeInfo))
   Console.WriteLine("  " & s)
   typeInfo.ReleaseFuncDesc(pFuncDesc)
Next

PARAMFLAG_FOUT のほかに、in、retval、optional などのフラグがあります。フィールドおよびメンバの両方の型情報が TYPEDESC 構造体に格納され、関数 DumpTypeDesc を呼び出してこれを出力しました。ITypeInfo でなく TYPEDESC を使用していることに驚かれるかもしれません。次で詳しく説明します。

プリミティブ型と合成型

COM は TYPEDESC を使用してある型を記述し、ITypeInfo を使用して別の型を記述します。違いは何でしょうか。COM は、タイプ ライブラリに定義されたクラスおよびインターフェイスにのみ ITypeInfo を使用します。整数や文字列などのプリミティブ型、および SpVoice 配列や IUnknown 参照などの複合型には TYPEDESC を使用します。

これは .NET と異なります。まず、.NET では整数や文字列などのプリミティブ型は System.Type を通じてクラスまたは構造体によって表現されます。2 番目に、.NET では整数配列などの複合型は System.Type を通じて表現されます。

TYPEDESC で掘り下げる必要のあるコードはかなり簡単です (図 8 を参照)。ここでも Case VT_USERDEFINED で参照へのハンドルが使用されているため、GetRefTypeInfo を通じて参照する必要があります。

図 8 TYPEDESC を調べる

Function DumpTypeDesc(ByVal tdesc As ComTypes.TYPEDESC, _
  ByVal context As ComTypes.ITypeInfo) As String
    Dim vt = CType(tdesc.vt, VarEnum)
    Select Case vt

        Case VarEnum.VT_PTR
            Dim tdesc2 = CType(Marshal.PtrToStructure(tdesc.lpValue, _
                          GetType(ComTypes.TYPEDESC)), ComTypes.TYPEDESC)
            Return "Ref " & DumpTypeDesc(tdesc2, context)

        Case VarEnum.VT_USERDEFINED
            Dim href = CType(tdesc.lpValue.ToInt64 And Integer.MaxValue, Integer)
            Dim refTypeInfo As ComTypes.ITypeInfo = Nothing
            context.GetRefTypeInfo(href, refTypeInfo)
            Dim refTypeName = ""
            refTypeInfo.GetDocumentation(-1, refTypeName, "", 0, "")
            Return refTypeName

        Case VarEnum.VT_CARRAY
            Dim tdesc2 = CType(Marshal.PtrToStructure(tdesc.lpValue, _
                          GetType(ComTypes.TYPEDESC)), ComTypes.TYPEDESC)
            Return "Array of " & DumpTypeDesc(tdesc2, context)
            ' lpValue is actually an ARRAYDESC structure, which also has
            ' information on the array dimensions, but alas .NET doesn't 
            ' predefine ARRAYDESC.

        Case Else
            ' There are many other VT_s that I haven't special-cased, 
            ' e.g. VT_INTEGER.
            Return vt.ToString()
    End Select
End Function

値の COM 表現

次の手順では、実際に COM オブジェクトをダンプします。つまり、プロパティの値を出力します。Visual Basic で次のように遅延バインディング呼び出しを使用できるため、これらのプロパティの名前がわかっていれば、作業は簡単です。

Dim com as Object : Dim val = com.SomePropName

このコードは、コンパイラによって、プロパティの値をフェッチする実行時呼び出し IDispatch::Invoke に変換されます。しかし、このリフレクションではプロパティ名がわからないかもしれません。手元にあるのが MEMBERID だけの場合も考えられるため、自分で IDispatch::Invoke を呼び出す必要があります。これは少し厄介です。

最初の悩みの種は、COM と .NET では値の表現方法がかなり異なることです。.NET では、任意の値を表現するのに Object を使用します。COM では、図 9 に示すように VARIANT 構造体が使用されます。

図 9 VARIANT を使用する

''' <summary>
''' VARIANT: this is called "Object" in Visual Basic. It's the universal ''' variable type for COM.
''' </summary>
''' <remarks>The "vt" flag determines which of the other fields have
''' meaning. vt is a VarEnum.</remarks>
<System.Runtime.InteropServices.StructLayoutAttribute( _
           System.Runtime.InteropServices.LayoutKind.Explicit, Size:=16)> _
Public Structure [VARIANT]
    <System.Runtime.InteropServices.FieldOffsetAttribute(0)> Public vt As UShort
    <System.Runtime.InteropServices.FieldOffsetAttribute(2)> _
      Public wReserved1 As UShort
    <System.Runtime.InteropServices.FieldOffsetAttribute(4)> _
      Public wReserved2 As UShort
    <System.Runtime.InteropServices.FieldOffsetAttribute(6)> _
      Public wReserved3 As UShort
    '
    <System.Runtime.InteropServices.FieldOffsetAttribute(8)> Public llVal As Long
    <System.Runtime.InteropServices.FieldOffsetAttribute(8)> Public lVal As Integer
    <System.Runtime.InteropServices.FieldOffsetAttribute(8)> Public bVal As Byte
    ' and similarly for many other accessors
    <System.Runtime.InteropServices.FieldOffsetAttribute(8)> _
      Public ptr As System.IntPtr

    ''' <summary>
    ''' GetObject: returns a .NET Object equivalent for this Variant.
    ''' </summary>
    Function GetObject() As Object
        ' We want to use the handy Marshal.GetObjectForNativeVariant.
        ' But this only operates upon an IntPtr to a block of memory.
        ' So we first flatten ourselves into that block of memory. (size 16)
        Dim ptr = Marshal.AllocCoTaskMem(16)
        Marshal.StructureToPtr(Me, ptr, False)
        Try : Return Marshal.GetObjectForNativeVariant(ptr)
        Finally : Marshal.FreeCoTaskMem(ptr) : End Try
    End Function
End Structure

COM 値は vt フィールドを使用して型を示します。VarEnum.VT_INT、VarEnum.VT_PTR、または 30 余りの VarEnum 型のいずれかになります。型がわかると、大きな Select Case ステートメントの中でほかにどのフィールドを参照したらよいかがわかります。さいわいなことに、この Select Case ステートメントは Marshal.GetObjectForNativeVariant 関数で既に実装されています。

COM オブジェクトのプロパティをダンプする

Visual Studio の "クイック ウォッチ" ウィンドウのような形式で、COM オブジェクトのプロパティをダンプする必要がある場合があります。

DUMP OF COM OBJECT #28114988
ISpeechVoice.Status = System.__ComObject   As Ref ISpeechVoiceStatus
ISpeechVoice.Rate = 0   As Integer
ISpeechVoice.Volume = 100   As Integer
ISpeechVoice.AllowAudioOutputFormatChangesOnNextSet = True   As Bool
ISpeechVoice.EventInterests = 0   As SpeechVoiceEvents
ISpeechVoice.Priority = 0   As SpeechVoicePriority
ISpeechVoice.AlertBoundary = 32   As SpeechVoiceEvents
ISpeechVoice.SynchronousSpeakTimeout = 10000   As Integer

問題は、COM には多数の異なる型が存在することです。すべてのケースを正しく処理するコードを作成することは非常に骨が折れる作業で、それらのすべてをテストするための十分なテスト ケースを作成することは困難です。ここでは、適切に処理できることがわかっている小さい型セットをダンプするだけにしておきます。

それ以外でダンプが役に立つことはあるでしょうか。プロパティのほかに、IsTall() などの純粋な (副作用のない) 関数を通じて公開される情報のダンプを取得することも有益です。しかし、AddRef() などの関数を呼び出したくはありません。これらの 2 つを区別するために、"Is*" などの関数名はダンプに適していると判断しました (図 10 を参照)。COM プログラマが Is* 関数を使用する頻度は .NET を使用するプログラマよりだいぶ少ないらしいことがわかりました。

図 10 Get* および Is* メソッドを調べる

' We'll only try to retrieve things that are likely to be side-effect-
' free properties:

If (funcDesc.invkind And ComTypes.INVOKEKIND.INVOKE_PROPERTYGET) = 0 _
   AndAlso Not funcName Like "[Gg]et*" _
   AndAlso Not funcName Like "[Ii]s*" _
   Then Continue For
If funcDesc.cParams > 0 Then Continue For
Dim returnType = CType(funcDesc.elemdescFunc.tdesc.vt, VarEnum)
If returnType = VarEnum.VT_VOID Then Continue For
Dim returnTypeName = DumpTypeDesc(funcDesc.elemdescFunc.tdesc, typeInfo)

' And we'll only try to evaluate the easily-evaluatable properties:
Dim dumpableTypes = New VarEnum() {VarEnum.VT_BOOL, VarEnum.VT_BSTR, _
           VarEnum.VT_CLSID, _ 
           VarEnum.VT_DECIMAL, VarEnum.VT_FILETIME, VarEnum.VT_HRESULT, _
           VarEnum.VT_I1, VarEnum.VT_I2, VarEnum.VT_I4, VarEnum.VT_I8, _
           VarEnum.VT_INT, VarEnum.VT_LPSTR, VarEnum.VT_LPWSTR, _
           VarEnum.VT_R4, VarEnum.VT_R8, _
           VarEnum.VT_UI1, VarEnum.VT_UI2, VarEnum.VT_UI4, VarEnum.VT_UI8, _
           VarEnum.VT_UINT, VarEnum.VT_DATE, _
           VarEnum.VT_USERDEFINED}
Dim typeIsDumpable = dumpableTypes.Contains(returnType)
If returnType = VarEnum.VT_PTR Then
    Dim ptrType = CType(Marshal.PtrToStructure( _
      funcDesc.elemdescFunc.tdesc.lpValue, _
                        GetType(ComTypes.TYPEDESC)), ComTypes.TYPEDESC)
    If ptrType.vt = VarEnum.VT_USERDEFINED Then typeIsDumpable = True
End If

このコードで、ダンプ可能と見なされる最後の型は VT_PTR から VT_USERDEFINED 型です。これにより、別のオブジェクトへの参照を返すプロパティの一般的なケースが処理されます。

IDispatch.Invoke を使用する

最後の手順は、MEMBERID で特定されたプロパティの読み取り、または関数の呼び出しです。図 11 に、これを行うコードを示します。ここでの重要なメソッドは IDispatch.Invoke です。最初の引数は、プロパティまたは呼び出す関数のメンバ ID です。変数 dispatchType は property-get の 2 または function-invoke の 1 のいずれかです。引数を取る関数を呼び出す場合は、dispParams 構造体も設定します。最後に、結果が varResult で返されます。前と同様に、VARIANT から .NET オブジェクトに変換するには GetObject を呼び出すだけで済みます。

図 11 プロパティを読み取る、または関数を呼び出す

' Here's how we fetch an arbitrary property from a COM object, 
' identified by its MEMBID.
Dim val As Object
Dim varResult As New [VARIANT]
Dim dispParams As New ComTypes.DISPPARAMS With {.cArgs = 0, .cNamedArgs = 0}
Dim dispatchType = If((funcDesc.invkind And _
   ComTypes.INVOKEKIND.INVOKE_PROPERTYGET)<>0, 2US, 1US)
idisp.Invoke(funcDesc.memid, IID_NULL, 0, dispatchType, dispParams, _
   varResult, IntPtr.Zero, 0)
val = varResult.GetObject()
If varResult.vt = VarEnum.VT_PTR AndAlso varResult.ptr <> IntPtr.Zero _ 
   Then
   Marshal.Release(varResult.ptr)
End If

ここで、Marshal.Release の呼び出しに注意してください。COM の一般的なパターンでは、関数がポインタを渡す場合は最初に AddRef を呼び出しますが、Release を呼び出すのも呼び出し側の役目です。.NET にはガベージ コレクションがあるため、より好都合です。

なお、IDispatch.Invoke の代わりに ITypeInfo.Invoke を使用することもできました。しかし、少しわかりづらくなります。COM オブジェクトの IUnknown インターフェイスを指している変数 "com" があるとします。また、com の ITypeInfo が SpeechLib.SpVoice であり、member-id 12 のプロパティを持つものとします。ITypeInfo.Invoke(com,12) を直接呼び出すことはできません。まず QueryInterface を呼び出して com の SpVoice インターフェイスを取得し、これに対し ITypeInfo.Invoke を呼び出す必要があります。このため、IDispatch.Invoke を使用する方が簡単です。

ここまで、ITypeInfo を使用した COM オブジェクトのリフレクションのしくみを見てきました。この方法は、相互運用型を持たない COM クラスで役立ちます。また、IDispatch.Invoke を使用して、VARIANT 構造体に格納されている COM の値を取得する方法についても見てきました。

System.Type から継承する、ITypeInfo および TYPEDESC の完全なラッパーを作れないかと思ったことがありました。これを使用すれば、.NET 型と同じリフレクションのコードを COM 型に対して使用できます。ところが最終的に、少なくとも筆者のプロジェクトでは、この種のラッパーはごくわずかなメリットしかないのに非常に厄介な作業でした。

リフレクションで可能な操作の詳細については、「よくあるパフォーマンスの落とし穴を避けて高速なアプリケーションを作成する」および「CLR 徹底解剖: リフレクションについての考察」を参照してください。

このコラムの執筆に力を貸してくれた Eric Lippert、Sonja Keserovic、および Calvin Hsia に感謝します。

ご意見やご質問は instinct@microsoft.com まで英語でお送りください。

Lucian Wischik は Visual Basic 仕様担当者です。Visual Basic コンパイラ チームに参加してからは、型推定、ラムダ、および一般的な共変性に関連する新機能に取り組んで来ました。また、Robotics SDK と同時実行性についても取り組み、それらに関するいくつかの学術論文も投稿しています。彼はケンブリッジ大学で同時実行性理論の博士号を取得しています。