Бейсиковые инстинкты

Изучение COM-объектов с помощью отражения

Лусиан Вышик (Lucian Wischik)

Загружаемый файл с кодом доступен в коллекции кода MSDN
Обзор кода в интерактивном режиме

Cодержание

Библиотеки типов и вызываемые оболочки времени выполнения
Если у типа отсутствует 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.

Библиотеки типов и вызываемые оболочки времени выполнения

Создадим проект для иллюстрации. Сначала создайте проект и добавьте ссылку COM, выбрав Проект>Добавить ссылку. Для этой статьи я буду использовать библиотеку SpeechLib «Microsoft Speech Object Library». На рис. 1 показаны соответствующие объекты и файлы, проверяемые при выполнении показанного ранее кода отражения.

fig01.gif

Рис. 1. Отражение в SpeechLib

Sapi.dll – это библиотека DLL с SpeechLib. Она размещается по следующему адресу: %windir%\system32\speech\common\sapi.dll. Эта библиотека DLL содержит реализацию COM-класса SpVoice и TypeLibrary, в которой содержатся все сведения отражения для нее. Библиотеки типов являются необязательными, но почти все COM-компоненты в вашей системы имеют их.

Библиотека Interop.SpeechLib.dll была автоматически создана Visual Studio при выборе Проект > Добавить ссылку. Генератор отражает библиотеку типов и создает тип взаимодействия для SpVoice. Этот тип является управляемым классом, содержащим управляемый метод для всех собственных COM-методов, найденных в библиотеке типов. Также можно создать сборку взаимодействия самостоятельно с помощью программы командной строки tlbimp.exe из Windows SDK. Экземпляр типа взаимодействия называется вызываемой оболочкой времени выполнения (RCW) и служит оболочкой для указателя на экземпляр COM-класса.

При выполнении следующей команды Visual Basic создается RCW (экземпляр типа взаимодействия) и экземпляр COM-класса SpVoice:

Dim b As New SpeechLib.SpVoice

Переменная "b" ссылается на RCW, поэтому при отражении кодом "b" действительно отражался управляемый эквивалент, созданный из библиотеки типов

Пользователям, развернувшим ConsoleApplication1.exe, также необходимо развернуть библиотеку Interop.SpeechLib.dll. (Однако Visual Studio 2010 позволит копировать тип взаимодействия непосредственно в ConsoleApplication1.exe. Это сильно упростит развертывание. Функция называется «Без основной сборки взаимодействия» (No-Primary-Interop-Assembly, No-PIA).)

Если у типа отсутствует RCW

Что произойдет, если отсутствует сборка взаимодействия для COM-объекта? Например, если сам COM-объект был создан через CoCreateInstance, или, как часто происходит, был вызван метод на COM-объекте и возвращен COM-объект, тип которого заранее не известен? Что, если создан управляемый подключаемый модуль для неуправляемого приложения, и приложение предоставило COM-объект? Что, если COM-объекты, которые должны быть созданы, были определены в реестре?

Все это предоставит ссылку IntPtr на COM-объект, а не ссылку объекта на его 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-объекта необходимо самостоятельно выполнить отражения для его библиотеки типов. Это можно сделать с помощью 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, как в объявлении CoCreateInstance, показанном на рис. 3.

Рис. 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 – это эквивалент System.Type для классов и интерфейсов COM. Он позволяет перечислять членов класса или интерфейса. В этом примере я собираюсь напечатать их; однако можно использовать ITypeInfo для поиска членов во время выполнения и их вызова или получения их свойств с помощью IDispatch. На рис. 4 показана работа ITypeInfo и все остальные необходимые структуры.

fig04.gif

Рис. 4. ITypeInfo and Type Information

Первым шагом является получение ITypeInfo для определенного COM-объекта. Было бы замечательно, если бы можно было использовать rcw.GetType(), но, к сожалению, при этом возвращаются сведения System.Type о самом RCW. Также было бы удобно использовать встроенную функцию 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

У вас может возникнуть вопрос, почему для атрибута InterfaceType IDispatch установлено значение 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. Поиск этого дескриптора выполняется функцией function 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 функции, чтобы напечатать их. Может показаться удивительным, что вместо ITypeInfo используется TYPEDESC. Я объясню это далее.

Примитивные и синтетические типы

COM использует TYPEDESC для описания одних типов, и ITypeInfo для описания других. В чем разница? COM использует ITypeInfo только для классов и интерфейсов, определенных в библиотеках типов. TYPEDESC используется для примитивных типов, таких как Integer и String, а также для составных типов, таких как Array SpVoice или IUnknown Reference.

Это отличается от .NET: во-первых, в .NET даже примитивные типы, такие как Integer и String, представлены классами или структурами через System.Type; во-вторых, в .NET составные типы, такие как Array 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-объекта, т.е. печать значений его свойств. Эта задача проста, если известны имена этих свойств, поскольку можно просто использовать вызов с поздним связыванием в Visual Basic:

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:

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. Его первый аргумент – идентификатор члена свойства или вызываемой функции. Переменная 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,", указывающая на интерфейс IUnknown COM-объекта. Также предположим, что ITypeInfo com – это SpeechLib.SpVoice со свойством с идентификатором члена 12. Прямой вызов ITypeInfo.Invoke(com,12) невозможен; сначала необходимо вызвать QueryInterface для получения интерфейса SpVoice com, а затем вызвать ITypeInfo.Invoke для него. Таким образом, проще использовать IDispatch.Invoke.

Итак, теперь вы знаете, как осуществлять отражение для COM-объектов с помощью ITypeInfo. Это полезно для классов COM, в которых отсутствуют типы взаимодействия. Также было показано использование IDispatch.Invoke для получения значений из COM, сохраненных в структуре VARIANT.

Меня интересовало создание полной оболочки для ITypeInfo и TYPEDESC, производной от System.Type. Она позволяет использовать код для типов .NET также и для отражения типов COM. Но в итоге, по крайне мере для моего проекта, работа над подобной оболочкой не принесла соответствующих результатов.

Дополнительные сведения о возможностях отражения см. в статьях «Как избежать обычных провалов в области производительности для создания быстрых приложений» и «CLR вдоль и поперек: Отражения на отражении».

Выражаю свою искреннюю благодарность Эрику Липперту (Eric Lippert), Соне Кесерович (Sonja Keserovic) и Кэлвину Хся (Calvin Hsia) за их помощь в написании статьи.

Вопросы и комментарии направляйте по адресу instinct@microsoft.com.

Лусиан Вышик - ведущий специалист по спецификации Visual Basic. После перехода в группу компиляторов Visual Basic он работал над новыми функциями, относящимися к выводу типов, лямбда-выражениям и общей ковариации. Он также работал над Robotics SDK и параллелизмом и опубликовал несколько научных статей по этой теме. Лусиан имеет степень кандидата филологических наук по теории параллелизма Кембриджского университета.