本文章是由機器翻譯。

基本技術

使用反射檢查 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。

型別程式庫和運行時可調用包裝

讓我們構建一個專案以供說明之用。 首先,創建專案並通過“Project”(專案)>“AddReference”(添加引用)命令添加一個 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(其中包括它的所有反射資訊)。 雖然 TypeLibrarie 是可選的,但系統中的幾乎所有 COM 元件都會有一個。

Interop.SpeechLib.dll 是 Visual Studio 通過“Project”(專案)>“AddReference”(添加引用)命令自動生成的。 此生成器將反射 TypeLibrary 並為 SpVoice 生成一個交互操作類型。 此類型是一個託管類,其中含有在 TypeLibrary 中找到的每個本機 COM 方法的託管方法。 您也可以使用 Windows SDK 中的 tlbimp.exe 命令列工具自己生成交互操作程式集。 交互操作類型的實例被稱為“運行時可調用包裝”(Runtime Callable Wrapper, 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 物件,這時該怎麼辦?

每種情況都將為您提供對 COM 物件的 IntPtr 引用,而不是對其 RCW 的物件引用。 當您圍繞該 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)

但更為常見的是使用封送處理聲明此函數,這樣封送拆收器就會自動調用 GetObjectForIUnknown 和 Release,如 圖 3 中的 CoCreateInstance 聲明所示。

圖 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.InterfaceIsUnknown,而不是設置為 ComInterfaceType.InterfaceIsIDisapatch。這是因為 InterfaceType 屬性工作表示的是該介面的繼承來源,而不是表示它究竟是什麼。

您有一個 ITypeInfo。現在是讀取它的時候了。請看一下圖 6,其中顯示了我將要實現用來轉儲類型資訊的函數。對於 GetDocumentation,第一個參數是 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 來輸出它。 使用 TYPEDESC 而不使用 ItypeInfo,這似乎有些令人驚訝。 下麵我將對此詳加闡述。

基元類型和綜合類型

COM 使用 TYPEDESC 來描述某些類型,而使用 ITypeInfo 來描述其他類型。 這有何區別?COM 僅對在型別程式庫中定義的類和介面使用 ITypeInfo。 它對基元類型(如整數型或字串)以及複合類型(如 SpVoice 陣列或 IUnknown 引用)使用 TYPEDESC。

這與 .NET 是不同的:首先,在 .NET 中,即使是基元類型(如整數型和字串)也是由類或結構通過 System.Type 來表示的;其次,在 .NET 中,複合類型(如 Integer 陣列)是通過 System.Type 來表示的。

您需要在 TYPEDESC 中深入挖掘的代碼非常簡單(請參見圖 8)。 請注意,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 物件,即輸出其屬性的值。 如果知道這些屬性的名稱,則此任務會非常簡單,因為您可以只使用後期綁定調用:

Dim com as Object : Dim val = com.SomePropName

編譯器會將其轉換成 IDispatch::Invoke 的運行時調用,以提取屬性的值。 但對於反射,您可能不知道屬性名稱。 或許您所掌握的只是 MEMBERID,因此必須自行調用 IDispatch::Invoke。 這並不是很方便。

第一個頭疼的問題源于這樣一個事實,即 COM 和 .NET 表示值的方式大相徑庭。 在 .NET 中,使用 Object 來表示任意值。 而在 COM 中,使用的是 VARIANT 結構,如圖 9 所示。

圖 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 物件的屬性

您可能會希望轉儲 COM 物件的屬性,或多或少類似于 Visual Studio 中的“Quick Watch”(快速監視)視窗:

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() 之類的函數。 要區分這兩種情況,我認為任何函數名稱(如 "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 是 2(對於 property-get)或 1(對於 function-invoke)。 如果您調用了接受參數的函數,則還需設置 dispParams 結構。 最後,結果將在 varResult 中返回。 像以前一樣,您只需對其調用 GetObject 並將 VARIANT 轉換為 .NET 物件即可。

圖 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 的垃圾收集功能可以讓我省很多事。

順便說一下,我本來可以使用 ITypeInfo.Invoke 來代替 IDispatch.Invoke。 但它有點讓人迷惑。 假設您有一個變數 "com",它指向 COM 物件的 IUnknown 介面。 假設 com 的 ITypeInfo 為 SpeechLib.SpVoice,它恰好有一個屬性的 member-id 為 12。 您不能直接調用 ITypeInfo.Invoke(com,12);必須先調用 QueryInterface 來獲取 com 的 SpVoice 介面,然後再對其調用 ITypeInfo.Invoke。 最後一點,使用 IDispatch.Invoke 會更容易一些。

現在您已經看到了如何通過 ITypeInfo 來反射 COM 物件。 這對於缺少交互操作類型的 COM 類非常有用。 而且您也瞭解了如何使用 IDispatch.Invoke 來從 COM 檢索存儲在 VARIANT 結構中的值。

我確實考慮過圍繞 ITypeInfo 和 TYPEDESC(繼承自 System.Type)創建一個完整的包裝。 通過它,使用者可以使用與 .NET 類型相同的代碼對 COM 類型進行反射。 但最終,至少是對我的專案而言,這種包裝需要付出大量的工作而收益卻微乎其微。

有關反射功能的詳細資訊,請參閱“ 避開常見的性能缺陷來創建高速應用程式”和“ CLR 全面透徹解析:反射之反思”。

誠摯地感謝 Eric Lippert、Sonja Keserovic 和 Calvin Hsia 對本專欄的大力協助。 **

請將您想詢問的問題和提出的意見發送至 instinct@microsoft.com

Lucian Wischik 是 Visual Basic 規範的發起人。 自從加入 Visual Basic 編譯器團隊以來,他一致致力於研究與類型推斷、lambda 和泛型協變有關的新功能。 他還參與過 Robotics SDK 和併發操作的研究工作,發表過多篇有關該主題的學術文章。 Lucian 擁有劍橋大學併發理論博士學位。