MSDN Magazin > Home > Ausgaben > 2007 > January >  Tiefe Einblicke in CLR: Einführung in COM-...
Tiefe Einblicke in CLR
Einführung in COM-Interop
Thottam R. Sriram

Codedownload verfügbar unter: CLRInsideOut2007_01.exe (1480 KB)
Browse the Code Online
COM ist eine großartige Technologie. Ein Aspekt der CLR (Common Language Runtime), der sie zu einer äußerst leistungsfähigen Plattform macht, ist die nahtlose Interaktion zwischen Microsoft® .NET-Anwendungen und nicht verwalteten COM-Komponenten. Bei meiner Suche im Internet fand ich jedoch nur wenige funktionierende Beispiele, die die grundlegenden Konzepte von COM-Interop demonstrieren. Der vorliegende Artikel soll diese Grundkonzepte vorstellen und damit nützliche, funktionierende Beispiele liefern, mit denen ein Benutzer direkt in diese Technologie einsteigen kann.
Ich beginne mit einem einfachen ATL-COM-Server (Active Template Library). Dabei versuche ich, zuerst über einen nicht verwalteten COM-Client und dann über einen verwalteten Client auf die Methoden auf diesem Server zuzugreifen. Ich werde die verschiedenen DLLs durchgehen, um die Übersetzung von nicht verwalteten Servern in verwaltete Server zu erläutern, und ich werde zeigen, wie der Zugriff auf eine exportierte Methode in einer nicht verwalteten DLL mittels P/Invoke abläuft. Der schwierigste Teil hierbei ist, das Marshallen komplexer Strukturen vorzunehmen. Dieser Aspekt würde einen eigenen Artikel (oder gar ein ganzes Buch) füllen und soll daher hier nicht näher betrachtet werden. Sie erfahren, wie nicht verwalteter Code einen Rückruf in verwalteten Code über Schnittstellen vornehmen kann. (Dies würde auch mit Delegaten funktionieren, was jedoch nicht Gegenstand dieses Artikels ist.)
Abschließend wird das Debuggen des COM-Interop-Projekts mithilfe öffentlicher Symbole erläutert. Damit erhalten Sie eine grundlegende Einführung in WinDbg.exe, in das nicht verwaltete Debuggen und in das verwaltete Debuggen mittels SOS. Der Stapel soll dabei von verwaltet zu unverwaltet vorgestellt werden, während Aufrufe in beiden Richtungen erfolgen.

Ein einfacher ATL-COM-Server
Als Erstes soll ein einfacher COM-Server geschrieben werden. Der Client hostet den vorgangsinternen Server und führt Methoden im Server aus.
Um die Entwicklung zu vereinfachen, soll ATL-COM zum Einsatz kommen. Erstellen Sie ein Verzeichnis mit der Bezeichnung „COMInteropSample“. Legen Sie dann ein neues Visual C++®-ATL-Projekt an, und nennen Sie es beispielsweise „MSDNCOMServer“. Deaktivieren Sie die Option „Neues Verzeichnis erstellen“, und übernehmen Sie die Standardeinstellungen für die verbleibenden Optionen.
Fügen Sie der Lösung eine neue Klasse/Schnittstelle hinzu. Öffnen Sie den Projektmappen-Explorer, falls dieser noch nicht angezeigt wird. Klicken Sie mit der rechten Maustaste auf „MSDNCOMServer“, und wählen Sie die Option „Hinzufügen“ | „Klasse“. Wählen Sie „ATL“ und „Einfaches ATL-Objekt“, und klicken Sie dann auf „Hinzufügen“. Geben Sie den Kurznamen „MyCOMServer“ für die Klasse ein, und übernehmen Sie die Standardeinstellungen für die übrigen Optionen. Klicken Sie auf „Fertig stellen“, um die Klasse hinzuzufügen.
Fügen Sie der für Sie erstellten Schnittstelle eine neue Methode hinzu. Öffnen Sie die Klassenansicht, klicken Sie mit der rechten Maustaste auf die Schnittstelle „IMyCOMServer“, und wählen Sie „Hinzufügen“ | „Methode hinzufügen“. Geben Sie einen Namen für die Methode ein, beispielsweise „MyCOMServerMethod“, und klicken Sie auf „Fertig stellen“.
Implementieren Sie nun die Methode in der Klasse. Wählen Sie „MyCOMServer.cpp“, und betrachten Sie die Quelle. Sie sehen die Methode, die mit dem Assistenten in der Schnittstelle in der CPP-Datei hinzugefügt wurde. Fügen Sie den folgenden Code in den TODO-Abschnitt ein:
wprintf_s(L"Inside MyCOMServerMethod");
Kompilieren Sie den Code, um die Server-DLL zu generieren.
Damit ist der ATL-COM-Server fertig. Dieser Server implementiert die Schnittstelle „IMyCOMServer“ in der Klasse „CMyCOMServer“ und schreibt eine Meldung in den Standardausgabestrom. Die GUID für die Schnittstelle ist in der IDL-Datei als Attribut zur Schnittstellendefinition definiert (siehe Abbildung 1).
[
    object,
    uuid(45FA03A3-FD24-41EB-921C-15204CAF68AE),
    nonextensible,
    helpstring("IMyCOMServer Interface"),
    pointer_default(unique)
]
interface IMyCOMServer : IUnknown {
    [helpstring("method MyCOMServerMethod")] 
    HRESULT(MyCOMServerMethod)( void);
};

[
    uuid(3BA5DF7B-F8EF-4EDE-A7B5-5E7D13764ACC),
    version(1.0),
    helpstring("MSDNCOMServer 1.0 Type Library")
]
library MSDNCOMServerLib
{
    importlib("stdole2.tlb");
    [
        uuid(2D6D2821-A7FB-4F99-A61A-2286A47CD4D1),
        helpstring("MyCOMServer Class")
    ]
    coclass MyCOMServer
    {
        [default] interface IMyCOMServer;
    };
};


Erstellen eines COM-Clients
Greifen Sie nun über einen systemeigenen (nicht verwalteten) Client auf den COM-Server zu. Aus Gründen der Einfachheit kommen auch hier wieder Visual Studio® und die zugehörigen Assistenten zum Einsatz. Erstellen Sie ein neues, leeres Visual C++-Projekt. Wählen Sie im Projektmappen-Explorer die Option „Quelldateien“, und fügen Sie eine neue C++-Datei hinzu. Geben Sie einen Namen ein (z. B. COMClient.cpp), und fügen Sie dann den Code aus Abbildung 2 in die Datei ein.
#include "MSDNCOMServer.h"
#include "wchar.h"
#include "MSDNCOMServer_i.c"

void main()
{
    // Initialize COM
    IMyCOMServer *pUnk = NULL;
    CoInitialize(NULL);

    // Create the com object
    HRESULT hr = CoCreateInstance(CLSID_MyCOMServer, 
        NULL, CLSCTX_ALL, IID_IMyCOMServer, (void **)&pUnk);
    if(S_OK != hr)
    {
        wprintf(L"Error creating COM object ... %d", hr);
        return;
    }

    // Call a method on the COM object
    pUnk->MyCOMServerMethod();

    // Free resources and uninitialize COM
    pUnk->Release();
    CoUninitialize();
    return;
}

Fügen Sie die Headerdateien des Servers (MSDNCOMServer.h und MSDNCOMServer_i.c) hinzu. Kompilieren Sie dann den Client, und führen Sie ihn aus. Auf der Konsole sollte nun die Meldung „Inside MyCOMServerMethod“ angezeigt werden. Diese Meldung stammt vom COM-Server.
Herzlichen Glückwunsch! Sie haben einen COM-Server erstellt und in eine DLL kompiliert, einen COM-Client erstellt und die COM-Servermethoden vom Client aufgerufen.

Konvertieren einer COM-DLL mittels Tlbimp
Greifen Sie nun über einen verwalteten Client auf die COM-DLL zu. Öffnen Sie eine Visual Studio-Eingabeaufforderung, und wechseln Sie zum Verzeichnis, in dem Sie die COM-DLL erstellt haben. Führen Sie nun den folgenden Befehl aus:
tlbimp MSDNCOMServer.dll
Tlbimp.exe ist das Typbibliothek-Importierprogramm aus dem .NET Framework SDK. Dieser Befehl gibt eine verwaltete DLL mit der Bezeichnung „MSDNCOMServerLIB.dll“ zurück, die als verwalteter Wrapper für die nicht verwaltete COM-DLL fungiert.
Der nicht verwaltete Client funktioniert also. Als Nächstes sollen die ursprüngliche, nicht verwaltete COM-DLL und die verwaltete DLL betrachtet werden, die mit Tlbimp.exe generiert wurden. Abbildung 3 zeigt einen Speicherauszug aus Oleview aus der ursprünglichen COM-DLL.
// Generated .IDL file (by the OLE/COM Object Viewer)
// typelib filename: MSDNCOMServer.dll
[
    uuid(3BA5DF7B-F8EF-4EDE-A7B5-5E7D13764ACC),
    version(1.0),
    helpstring("MSDNCOMServer 1.0 Type Library"),
    custom(DE77BA64-517C-11D1-A2DA-0000F8773CE9, 100663662),
    custom(DE77BA63-517C-11D1-A2DA-0000F8773CE9, 1158351043),
    custom(DE77BA65-517C-11D1-A2DA-0000F8773CE9, Created by MIDL version 
           6.00.0366 at Fri Sep 15 13:10:42 2006)
]
library MSDNCOMServerLib
{
    // TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
    importlib("stdole2.tlb");

    // Forward declare all types defined in this typelib
    interface IMyCOMServer;

    [
      uuid(2D6D2821-A7FB-4F99-A61A-2286A47CD4D1),
      helpstring("MyCOMServer Class")
    ]
    coclass MyCOMServer {
        [default] interface IMyCOMServer;
    };

    [
      odl,
      uuid(45FA03A3-FD24-41EB-921C-15204CAF68AE),
      helpstring("IMyCOMServer Interface"),
      nonextensible
    ]
    interface IMyCOMServer : IUnknown {
        [helpstring("method MyCOMServerMethod")]
        HRESULT_stdcall MyCOMServerMethod();
    };

};

Wenn Sie die verwaltete Bibliotheksausgabe von Tlbimp mit einem .NET-Decompiler betrachten (z. B. .NET Reflector von Lutz Roeder), erhalten Sie eine ähnliche Ausgabe wie in Abbildung 4.
namespace MSDNCOMServerLib
{
    [ComImport, Guid("45FA03A3-FD24-41EB-921C-15204CAF68AE"), 
     InterfaceType(1), TypeLibType(0x80)]
    public interface IMyCOMServer
    {
        [MethodImpl(MethodImplOptions.InternalCall, 
         MethodCodeType=MethodCodeType.Runtime)]
        void MyCOMServerMethod();
    }

    [ComImport, CoClass(typeof(MyCOMServerClass)), 
     Guid("45FA03A3-FD24-41EB-921C-15204CAF68AE")]
    public interface MyCOMServer : IMyCOMServer
    {
    }

    [ComImport, TypeLibType(2), 
     Guid("2D6D2821-A7FB-4F99-A61A-2286A47CD4D1"), 
     ClassInterface(0)]
    public class MyCOMServerClass : IMyCOMServer, MyCOMServer
    {
        // Methods
        [MethodImpl(MethodImplOptions.InternalCall, 
         MethodCodeType=MethodCodeType.Runtime)]
        public virtual extern void MyCOMServerMethod();
    }
}

Es hat sich einiges getan. Die Bibliothek „MSDNCOMServerLib“ wurde in den Namespace „MSDNCOMServerLib“ übersetzt. Außerdem wurde die Schnittstelle „IMyCOMServer“ mit der zugeordneten GUID beibehalten, und die Co-Klasse „MyCOMServer“ wurde in die öffentliche Schnittstelle „MyCOMServer“ übersetzt, die von IMyCOMServer abgeleitet ist, sowie in die Klasse „MyCOMServerClass“, mit der die Schnittstellen „IMyCOMServer“ und „MyCOMServer“ implementiert werden.

Schreiben eines verwalteten Clients
Bislang haben Sie erfahren, wie Sie einen COM-Server und einen nicht verwalteten COM-Client schreiben, und Sie haben die Übersetzung beim Konvertieren der nicht verwalteten DLL in eine verwaltete DLL mittels Tlbimp kennengelernt. Im Folgenden soll die Bibliothek, die von Tlbimp in verwalteten Code exportiert wurde, zum Schreiben eines verwalteten COM-Clients herangezogen werden:
using System;
using MSDNCOMServerLib;

class Test
{
    static void Main()
    {
        MyCOMServerClass comServer = new MyCOMServerClass();
        comServer.MyCOMServerMethod();
    }
}
Der verwaltete Code ist erstaunlich einfach und kompakt. Ich habe diesen Code in der Datei „ManagedClient.cs“ gespeichert und dann mit dem folgenden Befehl kompiliert:
csc ManagedClient.cs /reference:MSDNCOMServerLib.dll
Wenn Sie die ausführbare Datei „ManagedClient.exe“ starten, sollten Sie dasselbe Ergebnis erhalten wie beim nicht verwalteten Client.

Marshallen einer Struktur und einer Zeichenfolge
Nun erhält der Server einige Parameter, nämlich eine Zeichenfolge und eine Struktur. Hierfür sind Änderungen sowohl am Server selbst als auch an den Clients erforderlich. Definieren Sie dazu zunächst die Methode auf dem Server.
Bearbeiten Sie als Erstes die IDL-Datei, und nehmen Sie den Code aus Abbildung 5 in diese Datei auf. Ich habe eine Definition für eine Struktur hinzugefügt und dann die Deklaration von MyCOMServerMethod so geändert, dass zwei Parameter (eine Zeichenfolge und eine Struktur) akzeptiert werden.
struct MyTestStruct
{
    int nIndex;
    [string] WCHAR* sName;
    int nValue;
};

[
    object,
    uuid(45FA03A3-FD24-41EB-921C-15204CAF68AE),
    nonextensible,
    helpstring("IMyCOMServer Interface"),
    pointer_default(unique)
]
interface IMyCOMServer : IUnknown{
    [helpstring("method MyCOMServerMethod")] 
    HRESULT(MyCOMServerMethod)(
        BSTR *sTestString, struct MyTestStruct *pMyTestStruct);
};


Bearbeiten Sie als Nächstes die Headerdatei auf dem Server, und fügen Sie den nachstehenden Prototyp der Methode hinzu, die in der IDL definiert ist.
class ATL_NO_VTABLE CMyCOMServer :
    public CComObjectRootEx<CComSingleThreadModel>,
    public CComCoClass<CMyCOMServer, &CLSID_MyCOMServer>,
    public IMyCOMServer
{
public:
    STDMETHOD(MyCOMServerMethod)(
        BSTR *sTestString, struct MyTestStruct *pMyTestStruct);
};
Ändern Sie die Implementierung der Methode so ab, dass die Änderung des Prototyps übernommen und auch weiterer Code eingefügt wird, mit dem die übergebenen Zeichenfolgen bearbeitet werden. Der fertige Code ist in Abbildung 6 abgebildet. Damit sind die Änderungen am Server abgeschlossen, und der Server kann nun erneut kompiliert werden.
// MyCOMServer.cpp : Implementation of CMyCOMServer
#include "stdafx.h"
#include "MyCOMServer.h"

// CMyCOMServer
STDMETHODIMP CMyCOMServer::MyCOMServerMethod(
    BSTR *sString, struct MyTestStruct *pStruct)
{
    if((NULL == sString) || (NULL == pStruct) || (NULL == 
        pStruct->sName))
    {
        wprintf_s(L"Invalid input\r\n");
        return E_FAIL;
    }
    
    // Print the input to the COM Method
    wprintf_s(
        L"Inside MyCOMServerMethod: Param-1 = %s Param-2 = %s\r\n", 
        *sString, pStruct->sName);

    // Modifying the input string
    SysFreeString(*sString);
    *sString = SysAllocString(L"Changed String from COM Server");
    if(NULL == *sString)
    {
        wprintf_s(L"Allocation failed ...\r\n");
        return E_FAIL;
    }

    // Modifying the string in the structure
    CoTaskMemFree(pStruct->sName); 
    pStruct->sName = reinterpret_cast<WCHAR *>
        (CoTaskMemAlloc(sizeof
             (L"Changed Structure from COM Server")+10));
    if(NULL == pStruct->sName)
    {
        wprintf_s(L"Allocation failed ...\r\n");
        return E_FAIL;
    }

    size_t size = 0;
    size = wcslen(L"Changed Structure from COM Server");
    wcscpy_s(pStruct->sName, size+1, 
        L"Changed Structure from COM Server");

    //Print the changed strings in COM Server
    wprintf_s(
        L"Exiting MyCOMServerMethod: Param-1 = %s Param-2 = %s\r\n", 
        *sString, pStruct->sName);
    return S_OK;
}

Um auf die neue COM-Methode auf dem verwalteten Client zugreifen zu können, kopiere ich die DLL in dasselbe Verzeichnis, in dem sich der verwaltete Client befindet. Ich führe den nachfolgenden Befehl erneut aus, um die neue MSDNCOMServerLib.dll abzurufen, die nunmehr die neue Methode enthält.
tlbimp MSDNCOMServer.dll
Sie können die neue Methodensignatur und Implementierung in Ildasm abrufen, einem weiteren Tool aus dem .NET Framework SDK. Beim Anzeigen werden Sie erkennen, dass das Marshalltool den Aufruf um den Code aus Abbildung 7 ergänzt hat.
namespace MSDNCOMServerLib
{
    [ComImport, Guid("45FA03A3-FD24-41EB-921C-15204CAF68AE"), 
     InterfaceType(1), TypeLibType(0x80)]
    public interface IMyCOMServer
    {
        [MethodImpl(MethodImplOptions.InternalCall, 
         MethodCodeType=MethodCodeType.Runtime)]
        void MyCOMServerMethod(
            [MarshalAs(UnmanagedType.BStr)] ref string sTestString, 
            ref MyTestStruct pMyTestStruct);
    }

    [ComImport, CoClass(typeof(MyCOMServerClass)), 
     Guid("45FA03A3-FD24-41EB-921C-15204CAF68AE")]
    public interface MyCOMServer : IMyCOMServer { }

    [ComImport, TypeLibType(2), 
     Guid("2D6D2821-A7FB-4F99-A61A-2286A47CD4D1"), 
     ClassInterface(0)]
    public class MyCOMServerClass : IMyCOMServer, MyCOMServer
    {
        // Methods
        [MethodImpl(MethodImplOptions.InternalCall, 
         MethodCodeType=MethodCodeType.Runtime)]
        public virtual extern void MyCOMServerMethod(
            [MarshalAs(UnmanagedType.BStr)] ref string sTestString, 
            ref MyTestStruct pMyTestStruct);
    }

    [StructLayout(LayoutKind.Sequential, Pack=4)]
    public struct MyTestStruct
    {
        public int nIndex;
        [MarshalAs(UnmanagedType.LPWStr)]
        public string sName;
        public int nValue;
    }
}

Die Eingabeverweiszeichenfolge wird als „In, OUT BSTR“ gemarshallt, und die Struktur bleibt gleich, obwohl die Zeichenfolge innerhalb der Struktur als LPWStr gemarshallt ist.
Nachdem nun der Server und auch die verwaltete DLL aus dem Tlbimp-Satz bereit ist, können Sie den verwalteten Client so überarbeiten, dass dieser eine Zeichenfolge und eine Struktur übergibt. Prüfen Sie dann, ob sich der zurückgegebene Wert geändert hat. Abbildung 8 zeigt den geänderten Clientcode.
using System;
using MSDNCOMServerLib;
using System.Runtime.InteropServices;

class Test
{
    [STAThread]
    static void Main()
    {
        // Calling COM methods from managed Client
        String param1 = "Original String from Managed Client";

        MyTestStruct param2 = new MyTestStruct();
        param2.sName = "Original Structure from Managed Client";
        
        MyCOMServerClass comServer = new MyCOMServerClass();
        comServer.MyCOMServerMethod(ref param1, ref param2);

        Console.WriteLine("Param1: {0} Param2: {1}\r\n\r\n", 
            param1, param2.sName);
    }
}

Der verwaltete Client ist noch immer schlicht und einfach. Der Client erstellt die Zeichenfolge und eine Instanz der Struktur, die aus der nicht verwalteten DLL importiert wurde. Kompilieren Sie nun den obigen Code auf dieselbe Weise wie zuvor, und führen Sie dann die EXE-Datei aus. Nun sollte die folgende Ausgabe angezeigt werden:
Inside MsyCOMServerMethod: Param-1 = Original String from Managed Client 
Param-2 = Original Structure from Managed Client
Exiting MyCOMServerMethod: Param-1 = Changed String from COM Server 
Param-2 = Changed Structure from COM Server
Param1: Changed String from COM Server Param2: Changed Structure from COM Server
Der Aufruf aus Main in verwaltetem Code geht zum nicht verwalteten Server und sorgt dafür, dass die weitergeleitete Eingabe ausgegeben wird. Der Server ändert die Zeichenfolgen und gibt diese auf dem Server aus. Um zu prüfen, ob die geänderte Zeichenfolge zum Client zurückgegeben wurde, muss sie auch auf dem Client ausgegeben werden.
Um eine nicht verwaltete Funktion (Nicht-COM-Funktion) in derselben COM-DLL erstellen zu können, muss die Methode zunächst definiert und in der nicht verwalteten DLL implementiert werden. Wählen Sie „MSDNCOMServer.cpp“, und fügen Sie den nachstehenden Code am Ende dieser Datei ein.
STDAPI MyNonCOMMethod()
{
    wprintf_s(L"Inside MyNonCOMMethod\r\n");
    return true;
} 
Sie müssen die Methode aus der DLL exportieren, um darauf zugreifen zu können. Fügen Sie dazu dieser Funktion in der Datei „COMServer.def“ einen Eintrag hinzu:
; COMServer.def : Declares the module parameters.

LIBRARY      "COMServer.DLL"

EXPORTS
    DllCanUnloadNow       PRIVATE
    DllGetClassObject     PRIVATE
    DllRegisterServer     PRIVATE
    DllUnregisterServer   PRIVATE
    MyNonCOMMethod        PRIVATE
Kompilieren Sie den Server, und generieren Sie die neue DLL, die sowohl die COM-Methode als auch die exportierte Funktion enthält.
Kopieren Sie die DLL in das Verzeichnis, in dem sich der verwaltete Client befindet, und führen Sie Tlbimp darauf aus. Sie können die exportierte Funktion, die in der verwalteten DLL zur Verfügung steht, mittels Ildasm abrufen.

Verwenden von P/Invoke aus verwaltetem Code
Als Nächstes soll der verwaltete Client so geändert werden, dass die nicht verwaltete Funktion mit P/Invoke aufgerufen wird. Definieren Sie dazu die Methode in verwaltetem Code, und starten Sie dann den Aufruf. Die Deklaration der verwalteten Methode gibt die nicht verwaltete Deklaration wieder. Für die CLR ist hier außerdem ersichtlich, wo sich die eigentliche Implementierung befindet. Abbildung 9 zeigt die Änderungen am Code.
class Test
{
    [DllImport("MSDNCOMServer.dll")]
    static extern bool MyNonCOMMethod();

    [STAThread]
    static void Main()
    {
        // Calling COM methods from managed Client
        ...

        // Calling unmanaged function using pInvoke
        MyNonCOMMethod();
    }
}

Sie haben den ursprünglichen verwalteten Client um eine DllImport-Anweisung und einen Aufruf für MyNonCOMMethod ergänzt. Beim Kompilieren und Ausführen des Clients sollte in etwa die folgende Ausgabe entstehen:
Inside MyCOMServerMethod: Param-1 = Original String from Managed Client 
Param-2 = Original Structure from Managed Client
Exiting MyCOMServerMethod: Param-1 = Changed String from COM Server 
Param-2 = Changed Structure from COM Server
Param1: Changed String from COM Server Param2: Changed Structure from COM Server

Inside MyNonCOMMethod
Der Aufruf an MyNonCOMMethod wurde ausgeführt und hat die richtige Anweisung ausgegeben.

Aufrufen von verwaltetem Code mittels Schnittstellen
Abschließend soll ein verwalteter Aufruf von der nicht verwalteten Funktion aus über den umgekehrten P/Invoke-Befehl erfolgen. Diese Vorgehensweise ist nicht üblich, aber sehr nützlich. Sie können eine verwaltete Schnittstelle oder einen Delegaten an nicht verwalteten Code übergeben, und der nicht verwaltete Code kann den Delegaten oder die Methoden über die Schnittstelle aufrufen. Als Erstes soll die verwaltete Schnittstelle bereitgestellt werden, damit sie aus dem nicht verwalteten Code aufgerufen werden kann. Deklarieren Sie dazu die Schnittstelle in ManagedClient.cs (siehe Abbildung 10).
using System;
using MSDNCOMServerLib;
using System.Runtime.InteropServices;

[Guid("7DAC8207-D3AE-4c75-9B67-92801A497D44"), 
 InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[ComVisible(true)]
public interface IReversePInvoke
{
    void ManagedMethod(ref int val);
}

class ManagedClass : IReversePInvoke
{
    public void ManagedMethod(ref int nVal)
    {
        Console.WriteLine("ManagedMethod: " + nVal);
        nVal = 20;
        Console.WriteLine("ManagedMethod Value changed to: " + nVal);
    }
}

[DllImport("MSDNCOMServer.dll")]
static extern bool MyNonCOMMethod(IReversePInvoke ptr);

[STAThread]
static void Main()
{
    ...
    ManagedClass man = new ManagedClass();
    MyNonCOMMethod(man);
}

In diesem Code habe ich eine Deklaration für IReversePInvoke (eine Klasse, die die Schnittstelle implementiert) und auch eine geänderte DLLImport-Methode in der nicht verwalteten DLL hinzugefügt, damit diese einen Schnittstellenzeiger akzeptiert. Ich habe auch die Position geändert, an der die Methode aufgerufen wird, um die ManagedClass-Instanz an MyNonCOMMethod zu übergeben.
Sie könnten den Client jetzt schon kompilieren, allerdings würde er nicht erfolgreich ausgeführt werden, weil Sie beim Server noch nicht angegeben haben, dass er die Schnittstelle als Parameter akzeptieren soll. Führen Sie also Tlbexp (ein weiteres Tool aus dem .NET Framework SDK, mit dem eine Typbibliothek aus einer verwalteten Assembly erstellt wird) für die verwaltete EXE-Datei aus, sodass eine TLB-Datei entsteht. Öffnen Sie die Datei in Oleview, kopieren Sie den Prototyp der Schnittstelle, und fügen Sie ihn wie folgt in die Server-IDL-Datei ein:
[
    odl,
    uuid(7DAC8207-D3AE-4C75-9B67-92801A497D44)
]
interface IReversePInvoke : IUnknown {
    HRESULT _stdcall ManagedMethod([in, out] long* val);
};
Die Schnittstelle befindet sich nun auf dem Server. Ändern Sie die Signatur der Methode, und fügen Sie den Aufruf hinzu, mit dem die verwaltete Methode für die Schnittstelle aufgerufen wird. Gehen Sie wie folgt vor:
STDAPI MyNonCOMMethod(IReversePInvoke *ptr)
{
    int param = 10;
    
    wprintf_s(L"Inside MyNonCOMMethod\r\n");
    HRESULT hr = ptr->ManagedMethod(&param);
    wprintf_s(L"Value in MyNonCOMMethod = %d\r\n", param);
    wprintf_s(L"Exiting MyNonCOMMethod\r\n");
    
    return true;
}
Die nicht verwaltete Funktion akzeptiert nunmehr die verwaltete Schnittstelle und ruft eine Methode für die verwaltete Schnittstelle auf. Kompilieren Sie diesen Code in eine DLL, führen Sie Tlbimp für die DLL aus, sodass Sie eine aktualisierte verwaltete Interop-DLL erhalten, und kompilieren Sie den verwalteten Code mit der verwalteten DLL aus Tlbimp. Damit erhalten Sie die folgende Ausgabe:
Inside MyCOMServerMethod: Param-1 = Original String from Managed Client
Param-2 = Original Structure from Managed Client
Exiting MyCOMServerMethod: Param-1 = Changed String from COM Server 
Param-2 = Changed Structure from COM Server
Param1: Changed String from COM Server Param2: Changed Structure from 
COM Server

Inside MyNonCOMMethod
ManagedMethod: 10
ManagedMethod Value changed to: 20
Value in MyNonCOMMethod = 20
Exiting MyNonCOMMethod
Der Aufruf ist tatsächlich zum verwalteten Code zurückgelangt und hat wie erwartet den Wert „10“ ausgegeben. Die verwaltete Methode ändert diesen Wert in den Wert „20“ und gibt ihn in der nicht verwalteten Funktion aus.

Debuggen von verwaltetem und nicht verwaltetem Code
Im Prinzip müsste Ihr gesamter Code fehlerfrei funktioniert haben. Es gelingt jedoch nur selten, auf Anhieb vollkommen fehlerfreien Code zu schreiben. Irgendwann kommt der Zeitpunkt, an dem Sie wissen müssen, wie man Code debuggt, der Interop verwendet.
Damit das Debuggen problemlos läuft, stellen Sie sicher, dass sämtliche Symbole auf dem Computer geladen sind. Mithilfe der Symbole kann der Debugger die Vorgänge im Code besser verstehen und interpretieren. Microsoft stellt einen öffentlichen Server bereit, zu dem ein Debugger eine Verbindung herstellen kann, um auf diese Symbole für von Microsoft bereitgestellte DLLs zuzugreifen. Zur Konfiguration des windbg-Debuggers für die Verwendung dieses öffentlichen Symbolservers führen Sie zunächst windbg aus:
windbg managedclient.exe
Verwenden Sie dann die folgende Befehlsyntax, um windbg auf den Symbolserver zeigen zu lassen:
.SymFix
.sympath+ http://msdl.microsoft.com/download/symbols
.Reload
Als Nächstes sorgen Sie dafür, dass windbg beim Laden der DLLs „mscorwks“, „COMServerLib“ und „COMServer“ Haltepunkte setzt. Verwenden Sie dazu den Befehl „sxe ld“:
sxe ld mscorwks
sxe ld COMServerLib
sxe ld COMServer
Um die unter dem Debugger ausgeführte Anwendung zu starten, geben Sie den Buchstaben „g“ (für „go“) ein, und drücken Sie die Eingabetaste. Während der Ausführung unterbricht die Anwendung den Debugger, sobald die COMServer-DLL geladen wird. Zu diesem Zeitpunkt können Sie einen weiteren Haltepunkt für die MyCOMServerMethod festlegen (mit dem Befehl „bl“ können Sie dann prüfen, ob der Haltepunkt tatsächlich gesetzt wurde):
bp COMServer!CMyCOMServer::MyCOMServerMethod (wchar_t **, 
    struct MyTestStruct *)
Sobald mscorwks geladen ist, soll windbg die SOS-Debuggererweiterungen laden, die für eine deutlich stabilere Interaktion des Debuggers mit verwalteten Anwendungen sorgen:
.loadby sos mscorwks 
Um einen Haltepunkt für die ManagedMethod-Methode festzulegen, die vom COM-Server durch den umgekehrten P/Invoke-Befehl aufgerufen wird, führen Sie den Befehl „!bpmd“ aus SOS aus:
!bpmd ManagedClient ManagedClass.ManagedMethod
Mit diesem Satz an Haltepunkten können Sie nun erneut den Befehl „g“ ausführen und den Vorgang fortsetzen. Sobald der nicht verwaltete Haltepunkt in MyCOMServerMethod erreicht wird, können Sie die aktuelle Stapelverfolgung mit dem Befehl „windbg kL“ abrufen (siehe Abbildung 11). Hier sehen Sie auch, wie Sie die Werte der Variablen mit zusätzlichen windbg-Befehlen untersuchen können.
0:000> kL
ChildEBP RetAddr  
0012f328 79f21268 MSDNCOMServer!CMyCOMServer::MyCOMServerMethod
0012f3fc 0090a28e mscorwks!CLRToCOMWorker+0x196
0012f438 00f800e5 CLRStub[StubLinkStub]@90a28e
01271c08 79e88f63 ManagedClient!Test.Main()+0x75
0012f490 79e88ee4 mscorwks!CallDescrWorker+0x33
0012f510 79e88e31 mscorwks!CallDescrWorkerWithHandler+0xa3
0012f650 79e88d19 mscorwks!MethodDesc::CallDescr+0x19c
0012f668 79e88cf6 mscorwks!MethodDesc::CallTargetWorker+0x20
0012f67c 79f084b0 mscorwks!MethodDescCallSite::Call_RetArgSlot+0x18
0012f7e0 79f082a9 mscorwks!ClassLoader::RunMain+0x220
0012fa48 79f0817e mscorwks!Assembly::ExecuteMainMethod+0xa6
0012ff18 79f07dc7 mscorwks!SystemDomain::ExecuteMainMethod+0x398
0012ff68 79f05f61 mscorwks!ExecuteEXE+0x59
0012ffb0 79011b5f mscorwks!_CorExeMain+0x11b
0012ffc0 7c816fd7 mscoree!_CorExeMain+0x2c
0012fff0 00000000 KERNEL32!BaseProcessStart+0x23

0:000> dv
           this = 0x00fc2fb8
        sString = 0x0012f364
        pStruct = 0x00195ef8
           size = 0x12f330

0:000> dt sString
Local var @ 0x12f334 Type wchar_t**
0x0012f364 
 -> 0x001a61e4  "Original String from Managed Client"

0:000> dt -r pStruct
Local var @ 0x12f338 Type MyTestStruct*
0x00195ef8 
   +0x000 nIndex   : 0
   +0x004 sName    : 0x001a4f38  "Original Structure from Managed Client"
   +0x008 nValue   : 0

Wenn Sie wieder den Befehl „g“ ausführen, wird der Aufruf von ManagedMethod unterbrochen. Nun können Sie den verwalteten Stapel mit dem SOS-Befehl „!clrstack“ untersuchen:
0:000> !clrstack
OS Thread Id: 0x87c (0)
ESP       EIP     
0012ef80 00f80168 ManagedClass.ManagedMethod(Int32 ByRef)
0012f148 79f1ef33 [GCFrame: 0012f148] 
0012f2a0 79f1ef33 [ComMethodFrame: 0012f2a0] 
0012f454 79f1ef33 [NDirectMethodFrameSlim: 0012f454] Test.MyNonCOMMethod(IReversePInvoke)
0012f464 00f80119 Test.Main()
0012f69c 79e88f63 [GCFrame: 0012f69c]

Schlussbemerkung
Weitere Informationen finden Sie unter PInvoke.net, einer communitygestützten Ressource. Das Buch .NET and COM: The Complete Interoperability Guide über COM-Interop von Adam Nathan wurde neu aufgelegt. (Adam Nathans Blog finden Sie unter „Adam Nathan's Blog“.) Allgemeine Informationen zum Debuggen, beispielsweise die herunterladbare Datei und Referenzmaterial für WinDbg, finden Sie hier. Referenzmaterial für die SOS Debugging Extension (SOS.dll) finden Sie hier.

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


Thottam R. Sriram besitzt einen Master-Abschluss in Informatik von der Concordia University in Montreal (Kanada). Er arbeitet derzeit als Programm-Manager im CLR-Team, das sich mit COM-Interop beschäftigt. Vor dem Eintritt in das CLR-Team war Thottam R. Sriram im Windows-Team tätig. Wir danken den COM-Interop-Teammitgliedern Mason Bendixen, Claudio Caldato, Alessandro Catorcini, Ben Gillis und Snesha Arumugam für ihre Hilfe bei diesem Artikel.

Page view tracker