现在的位置: 首页 > 综合 > 正文

使用反射检查 COM 对象

2013年11月05日 ⁄ 综合 ⁄ 共 16123字 ⁄ 字号 评论关闭
Lucian Wischik
代码下载位置:MSDN 代码库
在线浏览代码
很多人在尝试让 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 声明所示。

<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 函数保留为空,因为目前不需要使用它;但您需要加入一个有关它的条目,因为此接口必须按正确的顺序列出正确的方法数。

''' <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,用于获取有关类型本身的信息。

''' <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 中(从托管代码访问此类非托管数组通常不会很顺畅,因为您必须执行指针算法)。

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 进行查找。

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 所示。

''' <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 程序员少很多!

' 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 对象即可。

' 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

抱歉!评论已关闭.