Lucian Wischik
很多人在尝试让 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%/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 中所示的内容。
图 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 应该如何应用以及您将需要使用的所有其他结构。
图 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 拥有剑桥大学并发理论博士学位。
from:MSDN