Qualifizieren von .NET-Typen für die COM-Interoperation

Verfügbarmachen von .NET-Typen für COM

Wenn Sie beabsichtigen, Typen in einer Assembly für eine COM-Anwendung verfügbar zu machen, beachten Sie zur Entwurfszeit die Anforderungen von Com-Interop. Verwaltete Typen (Klassen, Schnittstellen, Strukturen und Enumerationen) lassen sich nahtlos in COM-Typen integrieren, wenn Sie die folgenden Richtlinien einhalten:

  • Klassen sollten Schnittstellen explizit implementieren.

    COM-Interop stellt zwar einen Mechanismus bereit, mit dem automatisch eine Schnittstelle generiert wird, die alle Member einer Klasse und die Member der dazugehörigen Basisklasse enthält, dennoch ist es besser, explizite Schnittstellen bereitzustellen. Die automatisch generierte Schnittstelle wird als Klassenschnittstelle bezeichnet. Informationen zu Richtlinien finden Sie unter Einführung in die Klassenschnittstelle.

    Sie können Visual Basic, C# und C++ verwenden, um Schnittstellendefinitionen in Ihren Code einzuschließen. So müssen Sie nicht die Interface Definition Language (IDL) oder eine ähnliche Sprache verwenden. Einzelheiten zur Syntax finden Sie in der Dokumentation zur Sprache.

  • Verwaltete Typen müssen öffentlich sein.

    Nur öffentliche Typen in einer Assembly werden registriert und in die Typbibliothek exportiert. Dies hat zur Folge, dass nur öffentliche Typen für das COM sichtbar sind.

    Dank verwalteter Typen stehen Funktionen auch in anderem verwaltetem Code zur Verfügung, der möglicherweise nicht für COM verfügbar ist. COM-Clients haben z.B. keinen Zugriff auf parametrisierte Konstruktoren, statische Methoden und Konstantenfelder. Wenn die Runtime Daten in einen Typ hinein und aus einem Typ heraus marshallt, werden diese Daten möglicherweise kopiert oder umgewandelt.

  • Methoden, Eigenschaften, Felder und Ereignisse müssen öffentlich sein.

    Member öffentlicher Typen müssen ebenfalls öffentlich sein, wenn sie im COM sichtbar sein sollen. Sie können die Sichtbarkeit einer Assembly, eines öffentlichen Typs oder öffentlicher Member eines öffentlichen Typs einschränken, indem Sie die Klasse ComVisibleAttribute anwenden. Standardmäßig sind alle öffentlichen Typen und Member sichtbar.

  • Typen müssen über einen öffentlichen parameterlosen Konstruktor verfügen, um über COM aktiviert werden zu können.

    Verwaltete, öffentliche Typen sind im COM sichtbar. Ohne öffentlichen parameterlosen Konstruktor (Konstruktor ohne Argumente) können COM-Clients den Typ nicht erstellen. Wenn er auf andere Art und Weise aktiviert wird, können sie den Typ aber dennoch verwenden.

  • Typen können nicht abstrakt sein.

    Weder COM-Clients noch .NET-Clients können abstrakte Typen erstellen.

Beim Export eines verwalteten Typs in das COM wird seine Vererbungshierarchie vereinfacht. Auch die Versionsverwaltung läuft bei verwalteten und nicht verwalteten Umgebungen unterschiedlich ab. Im COM verfügbare Typen haben nicht dieselben Versionsverwaltungseigenschaften wie andere verwaltete Typen.

Nutzen von COM-Typen aus .NET

Wenn Sie COM-Typen aus .NET nutzen und keine Tools wie Tlbimp.exe (Type Library Importer) verwenden möchten, halten Sie sich an folgende Richtlinien:

  • Auf Schnittstellen muss ComImportAttribute angewendet werden.
  • Auf Schnittstellen muss GuidAttribute mit der Schnittstellen-ID für die COM-Schnittstelle angewendet werden.
  • Auf Schnittstellen muss InterfaceTypeAttribute werden, um den Basisschnittstellentyp dieser Schnittstelle (IUnknown, IDispatch oder IInspectable) anzugeben.
    • Die Standardoption besteht darin, den Basistyp IDispatch zu verwenden und die deklarierten Methoden an die erwartete virtuelle Funktionstabelle für die Schnittstelle anzufügen.
    • Nur .NET Framework unterstützt die Angabe des Basistyps IInspectable.

Diese Richtlinien enthalten die Mindestanforderungen für gängige Szenarien. Es gibt noch viele weitere Anpassungsoptionen. Diese werden unter Anwenden von Interop-Attributen beschrieben.

Definieren von COM-Schnittstellen in .NET

Wenn .NET-Code versucht, eine Methode für ein COM-Objekt über eine Schnittstelle mit dem Attribut ComImportAttribute aufzurufen, muss eine virtuelle Funktionstabelle (auch „vtable“ oder „vftable“ genannt) erstellt werden, um die .NET-Definition der Schnittstelle zu bilden und so den nativen Code zu bestimmen, der aufgerufen werden soll. Dieser Prozess ist komplex. In den folgenden Beispielen werden einige einfache Fälle gezeigt.

Als Beispiel dient eine COM-Schnittstelle mit einigen wenigen Methoden:

struct IComInterface : public IUnknown
{
    STDMETHOD(Method)() = 0;
    STDMETHOD(Method2)() = 0;
};

Die folgende Tabelle enthält eine Beschreibung des Layouts der virtuellen Funktionstabelle für diese Schnittstelle:

Slot der virtuellen Funktionstabelle für IComInterface Methodenname
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2

Die einzelnen Methoden werden der virtuellen Funktionstabelle in der Reihenfolge hinzugefügt, in der sie deklariert wurden. Die jeweilige Reihenfolge wird vom C++-Compiler definiert. In einfachen Fällen ohne Überladungen bestimmt allerdings die Deklarationsreihenfolge die Reihenfolge in der Tabelle.

Deklarieren Sie eine dieser Schnittstelle entsprechende .NET-Schnittstelle:

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
[Guid(/* The IID for IComInterface */)]
interface IComInterface
{
    void Method();
    void Method2();
}

InterfaceTypeAttribute gibt die Basisschnittstelle an. Sie bietet mehrere Optionen:

ComInterfaceType-Wert Art der Basisschnittstelle Verhalten für Member in der mit Attributen versehenen Schnittstelle
InterfaceIsIUnknown IUnknown Die virtuelle Funktionstabelle enthält zuerst die Member von IUnknown und anschließend die Member dieser Schnittstelle in der Deklarationsreihenfolge.
InterfaceIsIDispatch IDispatch Der virtuellen Funktionstabelle werden keine Member hinzugefügt. Auf sie kann nur über IDispatch zugegriffen werden.
InterfaceIsDual IDispatch Die virtuelle Funktionstabelle enthält zuerst die Member von IDispatch und anschließend die Member dieser Schnittstelle in der Deklarationsreihenfolge.
InterfaceIsIInspectable IInspectable Die virtuelle Funktionstabelle enthält zuerst die Member von IInspectable und anschließend die Member dieser Schnittstelle in der Deklarationsreihenfolge. Wird nur in .NET Framework unterstützt.

COM-Schnittstellenvererbung und .NET

Das COM-Interop-System, von dem ComImportAttribute verwendet wird, interagiert nicht mit der Schnittstellenvererbung. Daher kann es zu unerwartetem Verhalten kommen, wenn nicht ein paar Gegenmaßnahmen ergriffen werden.

Der COM-Quellgenerator, der das Attribut System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute verwendet, interagiert mit der Schnittstellenvererbung und verhält sich daher eher wie erwartet.

COM-Schnittstellenvererbung in C++

In C++ können Entwickler*innen wie folgt COM-Schnittstellen deklarieren, die von anderen COM-Schnittstellen abgeleitet werden:

struct IComInterface : public IUnknown
{
    STDMETHOD(Method)() = 0;
    STDMETHOD(Method2)() = 0;
};

struct IComInterface2 : public IComInterface
{
    STDMETHOD(Method3)() = 0;
};

Dieser Deklarationsstil wird gerne als Mechanismus verwendet, um COM-Objekten Methoden hinzuzufügen, ohne vorhandene Schnittstellen zu ändern, was ein Breaking Change wäre. Dieser Vererbungsmechanismus führt zu den folgenden Layouts der virtuellen Funktionstabelle:

Slot der virtuellen Funktionstabelle für IComInterface Methodenname
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
Slot der virtuellen Funktionstabelle für IComInterface2 Methodenname
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

Daher ist es einfach, eine in IComInterface definierte Methode über IComInterface2* aufzurufen. Insbesondere erfordert das Aufrufen einer Methode für eine Basisschnittstelle keinen Aufruf von QueryInterface, um einen Zeiger auf die Basisschnittstelle abzurufen. Darüber hinaus ermöglicht C++ eine implizite Konvertierung von IComInterface2* in IComInterface*. Diese ist klar definiert und ermöglicht die Vermeidung eines erneuten Aufrufs von QueryInterface. Folglich muss in C oder C++ nie QueryInterface aufgerufen werden, um zum Basistyp zu gelangen, wenn Sie das nicht möchten. Dies ermöglicht gewisse Leistungsverbesserungen.

Hinweis

Von WinRT-Schnittstellen wird dieses Vererbungsmodell nicht genutzt. Sie sind so definiert, dass sie das gleiche Modell verwenden wie das [ComImport]-basierte COM-Interop-Modell in .NET.

Vererbung von Schnittstellen mit ComImportAttribute

In .NET ist C#-Code, der wie eine Schnittstellenvererbung aussieht, eigentlich gar keine Schnittstellenvererbung. Betrachten Sie folgenden Code:

interface I
{
    void Method1();
}
interface J : I
{
    void Method2();
}

Dieser Code bedeutet nicht „J implementiert I“, sondern „Jeder Typ, der J implementiert, muss auch I implementieren“. Dieser Unterschied führt zu der grundlegenden Entwurfsentscheidung, die die Schnittstellenvererbung in ComImportAttribute-basiertem Interop unpraktisch macht. Schnittstellen werden immer für sich selbst betrachtet. Die Basisschnittstellenliste einer Schnittstelle hat keine Auswirkungen auf Berechnungen zur Ermittlung einer virtuellen Funktionstabelle für eine bestimmte .NET-Schnittstelle.

Daher führt die natürliche Entsprechung des vorherigen C++-COM-Schnittstellenbeispiels zu einem anderen Layout der virtuellen Funktionstabelle.

C#-Code:

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface
{
    void Method();
    void Method2();
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface2 : IComInterface
{
    void Method3();
}

Layouts der virtuellen Funktionstabellen:

Slot der virtuellen Funktionstabelle für IComInterface Methodenname
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
Slot der virtuellen Funktionstabelle für IComInterface2 Methodenname
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface2::Method3

Da sich diese virtuellen Funktionstabellen vom C++-Beispiel unterscheiden, kommt es zur Laufzeit zu schwerwiegenden Problemen. Die korrekte Definition dieser Schnittstellen in .NET mit ComImportAttribute lautet wie folgt:

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface
{
    void Method();
    void Method2();
}

[ComImport]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface2 : IComInterface
{
    new void Method();
    new void Method2();
    void Method3();
}

Auf Metadatenebene implementiert IComInterface2 nicht IComInterface, sondern gibt nur an, dass Implementierer von IComInterface2 auch IComInterface implementieren müssen. Daher muss jede Methode aus den Basisschnittstellentypen neu deklariert werden.

Schnittstellenvererbung mit GeneratedComInterfaceAttribute (.NET 8 und höher)

Der durch GeneratedComInterfaceAttribute ausgelöste COM-Quellgenerator implementiert die C#-Schnittstellenvererbung als COM-Schnittstellenvererbung, sodass sich für die virtuellen Funktionstabellen das erwartete Layout ergibt. Im Falle des vorherigen Beispiels lautet die korrekte Definition dieser Schnittstellen in .NET mit System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute wie folgt:

[GeneratedComInterface]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface
{
    void Method();
    void Method2();
}

[GeneratedComInterface]
[InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
interface IComInterface2 : IComInterface
{
    void Method3();
}

Die Methoden der Basisschnittstellen müssen (und sollten) nicht neu deklariert werden. In der folgenden Tabelle werden die resultierenden virtuellen Funktionstabellen beschrieben:

Slot der virtuellen Funktionstabelle für IComInterface Methodenname
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
Slot der virtuellen Funktionstabelle für IComInterface2 Methodenname
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

Wie Sie sehen, entsprechen diese Tabellen dem C++-Beispiel, sodass diese Schnittstellen ordnungsgemäß funktionieren.

Siehe auch