Compartilhar via


Instintos básicos

Inspecionando objetos COM com reflexão

Lucian Wischik

Código disponível para download na MSDN Code Gallery
Navegue pelo código online

Sumário

Bibliotecas de tipos e RCWs (Runtime Callable Wrappers)
Quando um tipo não tem um RCW
Usando ITypeInfo
Localizando referências de tipos
Obtendo os membros
Tipos primitivos e sintéticos
A representação COM de valor
Despejando as propriedades de um objeto COM
Usando IDispatch.Invoke
Discussão

Muitos de vocês já sentiram a frustração de tentar fazer com que o COM funcione. Também já sentiram a alegria de conseguir. Um truque comum para entender como um objeto funciona é inspecioná-lo usando os recursos de reflexão do Microsoft .NET Framework. Em alguns casos, a reflexão do .NET também funciona em objetos COM. Para entender o que estou dizendo, veja o seguinte código: O código usa a reflexão do .NET para obter e exibir uma lista de membros do objeto

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

e produz esta saída no console:

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

Mas esse código não funciona com todos os objetos COM. Para isso, é preciso usar a reflexão de COM. Esta coluna explica como e por quê.

Por que você talvez queira usar reflexão em um objeto? Eu descobri que a reflexão é útil para depurar e registrar em log; você pode usá-la para escrever uma rotina geral de "despejo" que imprima tudo o que for possível sobre um objeto. O código desta coluna será suficiente para que você escreva sua própria rotina de "despejo". Quando estiver pronta, você poderá chamá-la da janela Imediata durante a depuração. Isso é especialmente útil, já que o depurador do Visual Studio nem sempre fornece muitas informações sobre objetos COM.

Para uso na produção, a reflexão é útil se você tiver escrito um aplicativo que usa componentes de plug-in em que os usuários colocam seus componentes em um diretório, ou listam-nos no Registro, e seu aplicativo precisa examinar esses componentes e descobrir quais classes e métodos eles expõem. O Visual Studio, por exemplo, usa a reflexão desse modo para preencher o IntelliSense.

Bibliotecas de tipos e RCWs (Runtime Callable Wrappers)

Para ilustrar, vamos criar um projeto. Primeiro, crie o projeto e adicione uma referência COM via Projeto>Adicionar Referência. Nesta coluna usarei SpeechLib da "Microsoft Speech Object Library". A Figura 1 mostra as entidades e os arquivos relevantes que são examinados quando você executa o código de reflexão que viu anteriormente.

fig01.gif

Figura 1 Refletindo em SpeechLib

Sapi.dll é a DLL que contém SpeechLib. Ela costuma ficar em %windir%\system32\speech\common\sapi.dll. Essa DLL contém a implementação da classe COM SpVoice e uma Type­Library (biblioteca de tipos) que contém todas as informações de reflexão dela. As bibliotecas de tipos são opcionais, mas quase todo componente COM do sistema tem uma.

A Interop.SpeechLib.dll foi gerada automaticamente pelo Visual Studio via Projeto>Adicionar Referência. O gerador reflete na TypeLibrary e produz um tipo de interoperabilidade para SpVoice. Esse tipo é uma classe gerenciada que contém um método gerenciado para cada método COM nativo da TypeLibrary. Você próprio também pode gerar assemblies de interoperabilidade, usando a ferramenta de linha de comando tlbimp.exe do SDK do Windows. Uma instância de um tipo de interoperabilidade é chamada de RCW (runtime callable wrapper) e encapsula um ponteiro para uma instância de uma classe COM.

A execução do comando do Visual Basic a seguir cria um RCW (uma instância do tipo de interoperabilidade) e também uma instância da classe COM SpVoice:

Dim b As New SpeechLib.SpVoice

A variável "b" faz referência ao RCW, assim, quando o código refletiu em "b", ele estava na verdade refletindo no equivalente gerenciado, que tinha sido construído a partir da TypeLibrary.

Os usuários que implantarem o ConsoleApplication1.exe deverão implantar também a Interop.SpeechLib.dll. (No entanto, o Visual Studio 2010 permitirá que o tipo de interoperabilidade seja copiado diretamente no ConsoleApplication1.exe. Isso simplificará imensamente a implantação. O recurso é chamado de "Assembly de interoperabilidade não-primário" ou, abreviadamente, "Não-PIA".)

Quando um tipo não tem um RCW

O que acontece quando você não tem um assembly de interoperabilidade para um objeto COM? Por exemplo, e se você tiver criado o objeto COM via CoCreateInstance ou se, como acontece frequentemente, você chama um método em um objeto COM e ele retorna um objeto COM cujo tipo não é conhecido antecipadamente? E se você tiver escrito um plug-in gerenciado para um aplicativo não gerenciado, e o aplicativo tiver fornecido um objeto COM? E se você tiver descoberto, examinando o Registro, qual objeto COM criar?

Qualquer uma dessas hipóteses fornecerá uma referência IntPtr ao objeto COM em vez de uma referência do objeto a seu RCW. Quando você procura um RCW nesse IntPtr, obtém o que está ilustrado na Figura 2.

fig02.gif

Figura 2 Obtendo um RCW

Na Figura 2 você verá que o CLR forneceu um RCW padrão, uma instância do tipo de interoperabilidade padrão "System.__ComObject". Se você refletir nele deste modo

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

verá que ele não tem nenhum membro útil para você; ele tem apenas estes:

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()

Para obter reflexão nesse tipo de objeto COM, você mesmo precisa refletir na TypeLibrary. Você pode fazê-lo usando ITypeInfo.

Mas antes, uma observação: se um método retornar um objeto ou IDispatch ou ITypeInfo ou outra interface ou classe do .NET, ele terá dado a você uma referência ao RCW, e o .NET irá liberá-lo para você. Mas, se o método retornar um IntPtr, isso significará que você tem uma referência ao próprio objeto COM, e quase sempre você precisará chamar Marshal.Release nele (isso depende da semântica precisa do método que retornou o IntPtr). Veja como isso é feito:

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

Mas é muito mais comum declarar a função com marshaling, de modo que o marshaler chame GetObjectForI­Unknown e Release automaticamente, como na declaração de CoCreateInstance que você vê na Figura 3.

Figura 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)

Usando ITypeInfo

ITypeInfo é o equivalente a System.Type das interfaces e classes COM. Com ele, você pode enumerar os membros de uma classe ou interface. Neste exemplo, vou imprimi-los; no entanto, você pode usar ITypeInfo para pesquisar membros em tempo de execução e invocá-los ou obter os valores de suas propriedades por meio de IDispatch. A Figura 4 mostra como ITypeInfo combina, bem como todas as outras estruturas que você precisará usar.

fig04.gif

Figura 4 Informações sobre tipos e ITypeInfo

O primeiro passo é obter o ITypeInfo de determinado objeto COM. Seria bom se você pudesse usar rcw.GetType(), mas infelizmente isso retorna as informações de System.Type sobre o próprio RCW. Também seria bom se você pudesse usar a função interna Marshal.GetITypeInfoForType(rcw), mas infelizmente isso só funciona para RCWs provenientes de assemblies de interoperabilidade. Em vez disso, será necessário obter o ITypeInfo manualmente.

O código a seguir funcionará para ambos os casos, tanto para o RCW proveniente do stub em mscorlib quanto para o proveniente de um assembly de interoperabilidade apropriado:

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)

Esse código usa a interface IDispatch. A interface não é definida em nenhum lugar do .NET Framework, de modo que você precisará defini-la, como se vê na Figura 5. Deixei a função GetIDsOfNames vazia porque ela não é necessária para os objetivos atuais; de qualquer forma, você precisará incluir uma entrada para ela, porque a interface precisa listar o número correto de métodos na ordem correta.

Figura 5 Definindo a interface 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

Talvez você se pergunte porque o atributo InterfaceType de IDispatch está definido como ComInterfaceType.InterfaceIsUnknown e não como ComInterfaceType.InterfaceIsIDispatch. Isso acontece porque o atributo InterfaceType informa de que a interface é herdeira, não o que ela é.

Você tem um ITypeInfo. Agora é hora de começar a ler a partir dele. Observe a Figura 6, porque a função que vou implementar para despejar as informações do tipo está mostrada nela. Em GetDocumentation, o primeiro parâmetro é um MEMBERID, isto é, Get­Documentation destina-se a retornar informações sobre cada membro do tipo. Mas você também pode passar MEMBERID_NIL, cujo valor é -1, para obter informações sobre o tipo propriamente dito.

Figura 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

Observe como o marshaling funciona. Quando você chama typeInfo.GetTypeAttr, ele aloca um bloco não gerenciado de memória e retorna o ponteiro pTypeAttr. Então Marshal.PtrToStructure copia desse bloco não gerenciado para um bloco gerenciado (que será então coletado como lixo). Assim, não há problemas em chamar imediatamente type­Info.ReleaseTypeAttr.

Como mostrado anteriormente, você precisa de typeAttr para saber quantos membros e interfaces implementadas existem (typeAttr.cFuncs, typeAttr.cVars e typeAttr.cImplTypes).

Localizando referências de tipos

A primeira tarefa que deve ser concluída é obter uma lista das interfaces implementadas/herdadas. (No caso de COM, nunca se herda de outra classe). Eis o código:

' 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

Há aqui uma camada de referência indireta. GetRefTypeOfImplType não fornece diretamente o ITypeInfo dos tipos implementados: em vez disso, ele fornece um indicador para um ITypeInfo. A função GetRefType­Info é que pesquisa esse indicador. Então você pode usar o conhecido GetDocumentation(-1) para obter o nome do tipo implementado. Mais tarde falarei novamente sobre indicadores para ITypeInfo.

Obtendo os membros

Para refletir nos membros de campo, cada campo tem um VARDESC para descrevê-lo. Mais uma vez, o objeto typeInfo aloca um bloco de memória não gerenciado pVarDesc, e você o direciona para um bloco gerenciado varDesc e libera o bloco não gerenciado:

' 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

A função "GetNames" é curiosa. Conceitualmente, cada membro pode ter vários nomes. Basta obter o primeiro.

Geralmente, o código para refletir nos membros da função é semelhante (veja a Figura 7). O tipo de retorno é funcDesc.elemdescFunc.tdesc. O número de parâmetros formais é dado por funcDesc.cParams, e eles são armazenados na matriz funcDesc.lprgelemdescParam. (Não é confortável acessar, do código gerenciado, uma matriz não gerenciada como essa, porque você precisa usar aritmética de ponteiro.)

Figura 7 Refletindo nos membros da função

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

Existem outros sinalizadores, bem como PARAMFLAG_FOUT — sinalizadores de in, retval, optional etc. As informações de tipo para campos e membros foram armazenadas em uma estrutura TYPEDESC, e eu invoquei DumpTypeDesc para imprimi-las. Pode parecer surpreendente que TYPEDESC tenha sido usado em vez de ITypeInfo. Entrarei em detalhes a seguir.

Tipos primitivos e sintéticos

COM usa TYPEDESC para descrever alguns tipos e ITypeInfo para descrever outros. Qual é a diferença? COM usa ITypeInfo somente para classes e interfaces definidas nas bibliotecas de tipos. E usa TYPEDESC para tipos primitivos, como Integer ou String, e também para tipos compostos como Array of SpVoice ou IUnknown Reference.

Isso é diferente do .NET: primeiro, no .NET até os tipos primitivos como Integer e String são representados por classes ou estruturas por meio do System.Type; segundo, no .NET os tipos compostos como Array of Integer são representados por meio do System.Type.

O código de que você precisa para buscar via TYPEDESC é totalmente direto (veja a Figura 8). Observe que a condição Case VT_USERDEFINED usa novamente um identificador para uma referência, que você deve pesquisar por meio de GetRefTypeInfo.

Figura 8 Examinando 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

A representação COM de valor

A próxima etapa é realmente despejar um objeto COM, isto é, imprimir os valores de suas propriedades. Essa tarefa é fácil quando você sabe os nomes dessas propriedades, já que basta usar uma chamada de associação tardia no Visual Basic:

Dim com as Object : Dim val = com.SomePropName

O compilador transforma isso em uma chamada em tempo de execução de IDispatch::Invoke para buscar o valor da propriedade. Mas, no caso da reflexão, talvez você não saiba o nome da propriedade. Talvez tudo o que você tenha seja MEMBER­ID, de modo que você mesmo precisará chamar IDispatch::Invoke. Isso não é muito agradável.

A primeira dor de cabeça é que o COM e o .NET representam valores de modos muito diferentes. No .NET você usa Object para representar valores arbitrários. No COM, você usa a estrutura VARIANT, conforme mostra a Figura 9.

Figura 9 Usando 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

Um valor COM usa o campo vt para indicar qual é o tipo. Pode ser VarEnum.VT_INT ou VarEnum.VT_PTR ou qualquer um dos aproximadamente 30 tipos de VarEnum. Sabendo o tipo, você pode descobrir qual dos outros campos deve procurar em uma enorme instrução Select Case. Felizmente, essa instrução Select Case já foi implementada na função Marshal.GetObjectForNativeVariant.

Despejando as propriedades de um objeto COM

Você quererá despejar as propriedades do objeto COM, mais ou menos como a janela "Inspeção rápida" do Visual Studio:

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

O fato é que existem muitos tipos diferentes no COM. Seria exaustivo escrever código para tratar corretamente cada caso e difícil montar um número suficiente de casos de teste para testá-los todos. Aqui, vou me contentar em despejar um conjunto pequeno de tipos que sei que posso tratar corretamente.

E o que seria útil despejar, além disso? Além das propriedades, também seria útil obter um despejo de qualquer coisa exposta por funções puras (sem efeitos colaterais), como IsTall(). Mas você não iria querer invocar funções como AddRef(). Para distinguir entre essas duas, eu considero que qualquer nome de função como "Is*" é interessante para despejar (veja a Figura 10). Acontece que os programadores COM parecem usar as funções Is* com muito menos frequência do que os programadores que usam .NET!

Figura 10 Examinando os métodos Get* e 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

Nesse código, você considera que o tipo final que pode ser despejado é um VT_PTR para um tipo VT_USERDEFINED. Isso abrange o caso comum de uma propriedade que retorna uma referência a outro objeto.

Usando IDispatch.Invoke

A etapa final é ler a propriedade que você identificou pelo MEMBERID ou invocar a função. Veja o código para fazê-lo, na Figura 11. Aqui, o método principal é IDispatch.Invoke. O primeiro argumento é a identificação de membro da propriedade ou função que está sendo invocada. A variável dispatchType é 2 para um property-get (obter propriedade) ou 1 para um function-invoke (invocar função). Se você invocou uma função que continha argumentos, configurou também a estrutura dispParams. Por fim, o resultado vem em varResult. Como vimos antes, você pode simplesmente chamar Get­Object para converter VARIANT em um objeto .NET.

Figura 11. Ler a propriedade ou invocar a função

' 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

Observe a chamada para Marshal.Release. No COM, é padrão universal que, se uma função passa um ponteiro a alguém, ela primeiro chama AddRef, e que chamar Release é responsabilidade do chamador. Por isso fico mais contente ainda que o .NET tenha coleta de lixo.

A propósito, eu poderia ter usado ITypeInfo.Invoke em vez de IDispatch.Invoke. Mas isso gera um pouco de confusão. Suponha que você tenha uma variável, "com", que aponte para a interface IUnknown de um objeto COM. E suponha que o ITypeInfo dessa variável seja um SpeechLib.SpVoice que tenha uma propriedade com identificação de membro 12. Você não pode chamar ITypeInfo.Invoke(com,12) diretamente; precisa primeiro chamar Query­Interface para obter a interface da variável e, então, chamar ITypeInfo.Invoke. No final das contas, é mais fácil usar IDispatch.Invoke.

Agora você viu como refletir em objetos COM usando IType­Info. Isso é útil em classes COM que não têm tipos de interoperabilidade. E você viu como usar IDispatch.Invoke para recuperar valores de COM, armazenados em uma estrutura VARIANT.

Cheguei a pensar em criar um wrapper completo em torno de ITypeInfo e TYPEDESC, um que fosse herdeiro de System.Type. Assim, os usuários poderiam usar o mesmo código para reflexão em tipos COM e em tipos .NET. Mas no final, pelo menos em meu projeto, esse tipo de wrapper significaria muito trabalho para um ganho desprezível.

Para saber mais sobre o que a reflexão pode fazer por você, consulte "Para criar aplicativos rápidos, fuja das armadilhas comuns de desempenho" e "Tudo sobre o CLR: Reflexões sobre a reflexão".

Meus sinceros agradecimentos a Eric Lippert, Sonja Keserovic e Calvin Hsia por sua participação nesta coluna.

Envie perguntas e comentários para instinct@microsoft.com.

Lucian Wischik é o líder de especificações do Visual Basic. Desde que se juntou à equipe de compilação do Visual Basic, ele tem trabalhado em novos recursos relativos a inferência de tipos, lambdas e covariância genérica. Ele também trabalhou no SDK do Robotics e em simultaneidade, tendo publicado vários artigos acadêmicos sobre o assunto. Lucian é PhD em teoria da simultaneidade pela Universidade de Cambridge.