Freigeben über


Basic Instincts

Prüfen von COM-Objekten mit Reflektion

Lucian Wischik

Codedownload verfügbar in der MSDN-Codegalerie
Code online durchsuchen

Inhalt

Typbibliotheken und Runtime Callable Wrappers
Wenn einem Typ ein RCW fehlt
Verwenden von ITypeInfo
Verfolgen von Typverweisen
Abrufen der Member
Primitive und synthetische Typen
Die COM-Darstellung von Werten
Sichern der Eigenschaften eines COM-Objekts
Verwenden von IDispatch.Invoke
Diskussion

Viele von Ihnen kennen die Frustration bei dem Versuch, COM in Gang zu bringen. Sie kennen aber auch die Freude, wenn Sie erfolgreich sind. Ein gängiger Trick zum Verständnis der Funktionsweise eines Objekts besteht darin, dieses mit den Reflektionsfunktionen von Microsoft .NET Framework zu prüfen. In einigen Fällen funktioniert die .NET-Reflektion auch bei COM-Objekten. Im folgenden Code sehen Sie, was gemeint ist. Im Code wird .NET-Reflektion verwendet, um eine Liste der Member im Objekt zu erhalten und anzuzeigen.

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

Für die Konsole wird diese Ausgabe erzeugt:

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

Aber dieser Code funktioniert nicht für alle COM-Objekte. Dafür müssen Sie COM-Reflektion verwenden. In diesem Artikel werden das Warum und das Wie erklärt.

Warum sollten Sie Reflektion auf ein Objekt anwenden wollen? Ich finde, Reflektion ist nützlich beim Debuggen und Protokollieren. Sie können damit eine vielseitig verwendbare Sicherungsroutine schreiben, mit deren Hilfe alle möglichen Informationen über ein Objekt gedruckt werden. Der Code in diesem Artikel reicht aus, damit Sie Ihre eigene Sicherungsroutine schreiben können. Sobald dies erledigt ist, können Sie sie sogar während des Debuggens im Direktfenster aufrufen. Das ist besonders nützlich, da der Visual Studio-Debugger nicht immer viele Informationen zu COM-Objekten bietet.

Für die Verwendung in der Produktion ist Reflektion nützlich, wenn Sie eine Anwendung geschrieben haben, die Plug-In-Komponenten erfordert, wobei Benutzer ihre Komponenten in einem Verzeichnis ablegen oder in der Registrierung auflisten. Diese Komponenten müssen von Ihrer Anwendung untersucht werden, und es muss herausgefunden werden, welche Klassen und Methoden von ihnen verfügbar gemacht werden. Visual Studio z. B. verwendet Reflektion auf diese Weise, um IntelliSense aufzufüllen.

Typbibliotheken und Runtime Callable Wrappers

Erstellen wir ein Projekt zur Illustration. Erstellen Sie zuerst das Projekt, und fügen Sie über „Projekt“ > „Verweis hinzufügen“ einen COM-Verweis hinzu. Für diesen Artikel wird die „Microsoft Speech Object Library“-SpeechLib verwendet. In Abbildung 1 werden die relevanten Entitäten und Dateien gezeigt, die untersucht werden, wenn Sie den weiter oben stehenden Reflektionscode ausführen.

fig01.gif

Abbildung 1 Reflektieren über SpeechLib

Sapi.dll ist die DLL, die SpeechLib enthält. Sie befindet sich in „%windir%\system32\speech\common\sapi.dll“. Diese DLL enthält sowohl die Implementierung der COM-Klasse „SpVoice“ als auch eine Typbibliothek, die alle Reflektionsinformationen dafür enthält. TypeLibrarys sind optional, aber fast jede COM-Komponente in Ihrem System wird eine haben.

Interop.SpeechLib.dll wurde in Visual Studio durch „Projekt“ > „Verweis hinzufügen“ automatisch generiert. Mit dem Generator wird die TypeLibrary reflektiert und ein Interoptyp für SpVoice erzeugt. Dieser Typ ist eine verwaltete Klasse, die eine verwaltete Methode für jede systemeigene COM-Methode in der TypeLibrary enthält. Sie können Interopassemblys auch selbst generieren, indem Sie das Befehlszeilentool „tlbimp.exe“ aus dem Windows SDK verwenden. Eine Instanz eines Interoptyps wird Runtime Callable Wrapper (RCW) genannt. Dieser umschließt einen Zeiger auf eine Instanz einer COM-Klasse.

Durch das Ausführen des folgenden Visual Basic-Befehls wird ein RCW erstellt (eine Instanz des Interoptyps), und außerdem eine Instanz der COM-Klasse „SpVoice“:

Dim b As New SpeechLib.SpVoice

Mit der Variablen „b“ wird auf den RCW verwiesen. Als also mit dem Code also „b“ reflektiert wurde, wurde damit tatsächlich das verwaltete Äquivalent reflektiert, das von der TypeLibrary erstellt worden war.

Benutzer, die ihre ConsoleApplication1.exe bereitstellen, müssen auch Interop.SpeechLib.dll bereitstellen. (Visual Studio 2010 wird jedoch ermöglichen, dass der Interoptyp direkt in ConsoleApplication1.exe kopiert werden kann. Dadurch wird die Bereitstellung enorm vereinfacht. Das Feature wird „No-Primary-Interop-Assembly“ oder kurz „No-PIA“ genannt.)

Wenn einem Typ ein RCW fehlt

Was geschieht, wenn Sie keine Interopassembly für ein COM-Objekt haben? Was wäre zum Beispiel, wenn Sie das COM-Objekt selbst mittels CoCreateInstance erstellen oder, was oft geschieht, wenn Sie eine Methode für ein COM-Objekt aufrufen und ein COM-Objekt zurückgegeben wird, dessen Typ nicht im Voraus bekannt ist? Was wäre, wenn Sie ein verwaltetes Plug-In für eine nicht verwaltete Anwendung geschrieben haben und die Anwendung Ihnen ein COM-Objekt gegeben hat? Was wäre, wenn Sie beim Durchsehen der Registrierung erkennen könnten, welches COM-Objekt Sie erstellen müssen?

Durch all dies erhalten Sie einen IntPtr-Verweis auf das COM-Objekt statt eines Objektverweises auf dessen RCW. Was Sie erhalten, wenn Sie in diesem IntPtr nach einem RCW fragen, sehen Sie in Abbildung 2.

fig02.gif

Abbildung 2 Abrufen eines Runtime Callable Wrapper

In Abbildung 2 sehen Sie, dass die CLR einen standardmäßigen RCW bereitgestellt hat, eine Instanz des standardmäßigen Interoptyps „System.__ComObject“. Wenn Sie diesen folgendermaßen reflektieren:

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

werden Sie feststellen, dass er keine Member hat, die für Sie nützlich sind. Er hat nur diese:

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

Um eine nützliche Reflektion für ein solches COM-Objekt zu erhalten, müssen Sie seine TypeLibrary selbst reflektieren. Dies können Sie mit ITypeInfo erreichen.

Aber zunächst ein kurzer Hinweis: Wenn Ihnen mit einer Methode ein Object oder IDispatch oder ITypeInfo oder eine andere .NET-Klasse oder -Schnittstelle zurückgegeben wird, wurde Ihnen ein Verweis auf den RCW gegeben. .NET übernimmt die Veröffentlichung für Sie. Wenn Ihnen aber mit der Methode ein IntPtr zurückgegeben wird, bedeutet das, dass Sie einen Verweis auf das COM-Objekt selbst haben, und Sie müssen fast immer Marshal.Release dafür aufrufen (dies ist von der genauen Semantik der Methode abhängig, mit der Ihnen IntPtr gegeben wurde). Dies geschieht folgendermaßen:

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

Es ist bei weitem gängiger, die Funktion mithilfe von Marshalling zu deklarieren, damit der Marshaller GetObjectForIUnknown und Release automatisch aufruft. Dies ist in der Deklaration von CoCreateInstance in Abbildung 3 zu sehen.

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

Verwenden von ITypeInfo

ITypeInfo ist das Äquivalent von System.Type für COM-Klassen und -Schnittstellen. Damit können Sie die Member einer Klasse oder Schnittstelle aufzählen. In diesem Beispiel werden sie gedruckt. Mit ITypeInfo können Sie jedoch Member zur Laufzeit nachschlagen und dann aufrufen oder ihre Eigenschaftswerte mithilfe von IDispatch abrufen. In Abbildung 4 wird gezeigt, wie ITypeInfo sich einpasst, sowie alle anderen Strukturen, die Sie verwenden müssen.

fig04.gif

Abbildung 4 ITypeInfo und Typinformationen

Der erste Schritt ist das Abrufen von ITypeInfo für ein gegebenes COM-Objekt. Es wäre schön, wenn Sie rcw.GetType() verwenden könnten, aber leider werden dadurch die System.Type-Informationen zum RCW selbst zurückgegeben. Es wäre auch schön, wenn Sie die integrierte Funktion „Marshal.GetITypeInfoForType(rcw)“ verwenden könnten, aber leider funktioniert dies nur bei RCWs, die aus Interopassemblys kommen. Stattdessen müssen Sie ITypeInfo manuell abrufen.

Der folgende Code funktioniert in beiden Fällen, gleichgültig, ob der RCW vom Stub in mscorlib oder einer richtigen Interopassembly stammt:

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)

Bei diesem Code wird die Schnittstelle „IDispatch“ verwendet. Die Schnittstelle ist nirgendwo in .NET Framework definiert. Sie müssen sie also selbst definieren, wie in Abbildung 5 gezeigt. Die Funktion „GetIDsOfNames“ wurde leer gelassen, da sie momentan nicht gebraucht wird. Sie müssen jedoch einen Eintrag dafür haben, da mit der Schnittstelle die richtige Anzahl der Methoden in der richtigen Reihenfolge aufgelistet werden muss.

Abbildung 5 Definieren der IDispatch-Schnittstelle

''' <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

Unter Umständen fragen Sie sich, warum das InterfaceType-Attribut von IDispatch auf „ComInterfaceType.InterfaceIsUnknown“ statt auf „ComInterfaceType.InterfaceIsIDisapatch“ gesetzt ist. Das ist so, weil das InterfaceType-Attribut aussagt, wovon die Schnittstelle erbt, nicht, was sie ist.

Sie haben ein ITypeInfo. Nun ist es an der Zeit, daraus zu lesen. Sehen Sie sich Abbildung 6 an. Dort wird die Funktion angezeigt, die implementiert wird, um die Typinformationen zu sichern. Der erste Parameter für GetDocumentation ist ein MEMBERID. Das heißt, mit GetDocumentation sollen Informationen zu jedem Member des Typs zurückgegeben werden. Sie können auch MEMBERID_NIL übergeben, das den Wert -1 hat, um Informationen zum Typ selbst abzurufen.

Abbildung 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

Beachten Sie, wie das Marshalling funktioniert. Wenn Sie typeInfo.GetTypeAttr aufrufen, wird ein nicht verwalteter Speicherblock zugeordnet, und Sie werden an den Zeiger „pTypeAttr“ zurückgeleitet. Dann wird Marshal.PtrToStructure aus diesem nicht verwalteten Block in einen verwalteten Block kopiert, der dann von der Garbage Collection aufgenommen wird. Es ist also in Ordnung, sofort typeInfo.ReleaseTypeAttr aufzurufen.

Wie zuvor gezeigt, benötigen Sie typeAttr, um zu wissen, wie viele Member und implementierte Schnittstellen es gibt (typeAttr.cFuncs, typeAttr.cVars und typeAttr.cImplTypes).

Verfolgen von Typverweisen

Die erste Aufgabe, die abgeschlossen werden muss, besteht darin, eine Liste implementierter bzw. geerbter Schnittstellen abzurufen. (In COM wird nie von einer anderen Klasse geerbt.) Hier ist der Code:

' 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

Hier gibt es eine Dereferenzierungsschicht. GetRefTypeOfImplType gibt Ihnen das ITypeInfo der implementierten Typen nicht direkt: Stattdessen erhalten Sie ein Handle zu einem ITypeInfo. Dieses Handle wird mit der Funktion „GetRefTypeInfo“ nachgeschlagen. Dann können Sie mit dem Namen dieses implementierten Typs das vertraute „GetDocumentation(-1)“ abrufen. Die Handles zu ITypeInfo werden später erörtert.

Abrufen der Member

Um Feldmember reflektieren zu können, hat jedes Feld ein VARDESC, um es zu beschreiben. Wieder wird mit dem typeInfo-Objekt ein nicht verwalteter Speicherblock „pVarDes“ zugeordnet, den Sie in einen verwalteten Block „varDesc“ marshallen, und Sie veröffentlichen den nicht verwalteten Block:

' 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

Die Funktion „GetNames“ ist interessant. Es ist vorstellbar, dass jeder Member mehrere Namen hat. Es reicht aus, lediglich den ersten abzurufen.

Der Code zum Reflektieren von Funktionsmembern ist im Allgemeinen ähnlich (siehe Abbildung 7). Der Rückgabetyp ist „funcDesc.elemdescFunc.tdesc“. Die Anzahl der formellen Parameter wird von funcDesc.cParams gegeben, und formelle Parameter werden im Array „funcDesc.lprgelemdescParam“ gespeichert. (Es ist nicht erfreulich, aus verwaltetem Code auf ein nicht verwaltetes Array wie dieses zuzugreifen, weil Sie die Zeigerarithmetik durchführen müssen.)

Abbildung 7 Reflektieren von Funktionsmembern

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

Es gibt weitere Kennzeichen sowie PARAMFLAG_FOUT: Kennzeichen für „in“, „retval“, „optional“ und so weiter. Typinformationen sowohl für Felder als auch für Member wurden in einer TYPEDESC-Struktur gespeichert, und ich habe die Funktion „DumpTypeDesc“ aufgerufen, um diese zu drucken. Es mag überraschen, dass TYPEDESC statt ITypeInfo verwendet wird. Dies wird in Kürze erläutert.

Primitive und synthetische Typen

In COM wird TYPEDESC zum Beschreiben einiger Typen verwendet und ITypeInfo zum Beschreiben anderer Typen. Worin besteht der Unterschied? In COM wird ITypeInfo nur für Klassen und Schnittstellen verwendet, die in Typbibliotheken definiert sind. Außerdem wird TYPEDESC für primitive Typen wie Integer oder String verwendet, und auch für zusammengesetzte Typen wie Array von SpVoice oder IUnknown Reference.

Dies ist anders als in .NET: Erstens werden in .NET sogar die primitiven Typen wie Integer oder String durch System.Type von Klassen oder Strukturen repräsentiert. Zweitens werden in .NET zusammengesetzte Typen wie Arrays ganzer Zahlen durch System.Type repräsentiert.

Der Code, den Sie benötigen, um ein TYPEDESC zu durchsuchen, ist ziemlich einfach (siehe Abbildung 8). Beachten Sie, dass im Fall von VT_USERDEFINED wieder ein Handle für einen Verweis verwendet wird, den Sie durch GetRefTypeInfo nachschlagen müssen.

Abbildung 8 Betrachtung von 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

Die COM-Darstellung von Werten

Der nächste Schritt besteht darin, tatsächlich ein COM-Objekt zu sichern, also die Werte seiner Eigenschaften zu drucken. Diese Aufgabe ist einfach, wenn Sie die Namen dieser Eigenschaften kennen, da Sie einfach einen Aufruf mit später Bindung in Visual Basic verwenden können.

Dim com as Object : Dim val = com.SomePropName

Mit dem Compiler wird dies in einen Laufzeitaufruf von IDispatch::Invoke übersetzt, um den Wert der Eigenschaft abzurufen. Aber im Fall der Reflektion kennen Sie möglicherweise nicht den Eigenschaftsnamen. Vielleicht haben Sie nur MEMBERID, sodass Sie IDispatch::Invoke selbst aufrufen müssen. Das ist nicht sehr schön.

Die ersten Kopfschmerzen haben ihren Ursprung in der Tatsache, dass COM und .NET Werte auf sehr unterschiedliche Weise repräsentieren. In .NET verwenden Sie Object, um willkürliche Werte zu repräsentieren. In COM verwenden Sie die VARIANT-Struktur, siehe Abbildung 9.

Abbildung 9 Verwenden von 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

Von einem COM-Wert wird das vt-Feld verwendet, um anzuzeigen, welcher Typ es ist. Es kann VarEnum.VT_INT oder VarEnum.VT_PTR sein, oder ein anderer der etwa 30 VarEnum-Typen. Wenn Sie seinen Typ kennen, können Sie mit einer riesigen Select Case-Anweisung herausfinden, welche weiteren Felder nachgeschlagen werden müssen. Zum Glück wurde die Select Case-Anweisung bereits in der Marshal.GetObjectForNativeVariant-Funktion implementiert.

Sichern der Eigenschaften eines COM-Objekts

Wahrscheinlich möchten Sie die Eigenschaften Ihres COM-Objekts sichern, mehr oder weniger wie das Schnellansichtsfenster in 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

Das Problem liegt darin, dass es in COM viele verschiedene Typen gibt. Es wäre äußerst anstrengend, Code zu schreiben, um jeden Fall richtig zu verarbeiten, und schwierig, genügend Testfälle zusammenzustellen, um sie alle zu testen. Ich begnüge mich hier mit dem Sichern eines kleinen Satzes von Typen, von dem ich weiß, dass ich ihn richtig verarbeiten kann.

Was wäre darüber hinaus nützlich? Neben den Eigenschaften wäre es auch nützlich, eine Sicherung von allem zu haben, was durch reine (nebenwirkungsfreie) Funktionen wie IsTall() verfügbar gemacht wird. Aber Sie möchten sicher keine Funktionen wie AddRef() aufrufen. Um zwischen diesen beiden zu unterscheiden, ist ein beliebiger Funktionsname wie „Is*“ hervorragend für die Sicherung geeignet (siehe Abbildung 10). Wie sich herausstellt, scheinen COM-Programmierer Is* viel weniger zu verwenden als Programmierer, die .NET verwenden.

Abbildung 10 Betrachtung von Get*- und Is*-Methoden

' 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

Die letzte Art sicherbarer Typen in diesem Code, die Sie erwägen, ist ein VT_PTR für einen VT_USERDEFINED-Typ. Dadurch wird der allgemeine Fall einer Eigenschaft abgedeckt, mit der ein Verweis auf ein anderes Objekt zurückgegeben wird.

Verwenden von IDispatch.Invoke

Der letzte Schritt besteht darin, die Eigenschaft zu lesen, die Sie durch ihre MEMBERID identifiziert haben, oder die Funktion aufzurufen. Den Code dafür sehen Sie in Abbildung 11. Die Hauptmethode ist IDispatch.Invoke. Deren erstes Argument ist die Member-ID der Eigenschaft oder Funktion, die Sie aufrufen. Die Variable „dispatchType“ lautet entweder 2 für ein property-get oder 1 für ein function-invoke. Wenn Sie eine Funktion aufgerufen haben, die Argumente erforderte, würden Sie auch die dispParams-Struktur einrichten. Abschließend kommt das Ergebnis in varResult zurück. Wie zuvor müssen Sie lediglich GetObject dafür aufrufen, um VARIANT in ein .NET-Objekt zu konvertieren.

Abbildung 11 Lesen der Eigenschaft bzw. Aufrufen der Funktion

' 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

Beachten Sie den Aufruf an Marshal.Release. Dies ist ein universelles Muster in COM: Wenn eine Funktion jemandem einen Zeiger übergibt, wird zuerst AddRef dafür aufgerufen. Es obliegt dem Aufrufer, Release dafür aufzurufen. Das macht mich sogar noch glücklicher als die Garbage Collection von .NET.

Nebenbei bemerkt hätte ITypeInfo.Invoke statt IDispatch.Invoke verwendet werden können. Aber das wird ein wenig verwirrend. Angenommen, Sie haben eine Variable „com“, mit der auf die IUnknown-Schnittstelle eines COM-Objekts gezeigt wird. Weiterhin angenommen, das ITypeInfo von com ist ein SpeechLib.SpVoice, das eine Eigenschaft mit der Member-ID 12 hat. Sie können ITypeInfo.Invoke(com,12) nicht direkt aufrufen. Sie müssen zuerst QueryInterface aufrufen, um die SpVoice-Schnittstelle von com zu erhalten. Dafür können Sie dann ITypeInfo.Invoke aufrufen. Letztendlich ist es einfacher, IDispatch.Invoke zu verwenden.

Sie haben nun gesehen, wie COM-Objekte durch ITypeInfo reflektiert werden. Dies ist nützlich für COM-Klassen, denen Interoptypen fehlen. Außerdem haben Sie gesehen, wie mit IDispatch.Invoke Werte von COM abgerufen werden, die in einer VARIANT-Struktur gespeichert sind.

Ich habe mich gefragt, ob ich einen vollständigen Wrapper um ITypeInfo und TYPEDESC herum erstellen soll, der von System.Type erbt. Damit könnten Benutzer sowohl bei COM- als auch bei .NET-Typen den gleichen Code für die Reflektion verwenden. Aber letztendlich, zumindest für das vorliegende Projekt, würde diese Art von Wrapper zu viel Arbeit für zu wenig Nutzen bedeuteten.

Weitere Informationen zur Reflektion finden Sie in Vermeiden häufiger Leistungsfallen beim Erstellen schneller Anwendungen und in Tiefe Einblicke in CLR: Reflexionen über Reflektion.

Aufrichtiger Dank gilt Eric Lippert, Sonja Keserovic und Calvin Hsia für ihre Hilfe bei diesem Artikel.

Senden Sie Fragen und Kommentare (in englischer Sprache) an instinct@microsoft.com.

Lucian Wischik ist der Visual Basic Specification Lead. Seit er dem Visual Basic-Compilerteam beigetreten ist, hat er an neuen Features bezüglich Typrückschluss, Lambda-Ausdrücken und generischer Kovarianz gearbeitet. Er hat auch am Robotics SDK und an Gleichzeitigkeit gearbeitet und mehrere akademische Artikel zu diesem Thema veröffentlicht. Lucian Wischik besitzt einen PhD in Gleichzeitigkeitstheorie von der University of Cambridge.