本文配套源碼
內容
類型庫和運行時可調用包裝
當某個類型缺少 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 顯示了在運行您先前看到的反射代碼時需要檢查的相關實體和文件。
圖 1 關於 SpeechLib 的反射
Sapi.dll 是包含 SpeechLib 的 DLL。它恰好位於 %windir% system32speechcommonsapi.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 中所示的內容。
圖 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 聲明所示。
圖 3CoCreateInstance
<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 應該如何應用以及您將需要使用的所 有其他結構。
圖 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 類型進行反射。但最終,至少是對我的項目而言,這種包裝需要付出大量的工作 而收益卻微乎其微。