Freigeben über


Lernprogramm: Verwenden der ComWrappers API

In diesem Lernprogramm erfahren Sie, wie Sie den ComWrappers Typ ordnungsgemäß unterklassigen, um eine optimierte und AOT-freundliche COM-Interoperabilitätslösung bereitzustellen. Bevor Sie mit diesem Lernprogramm beginnen, sollten Sie mit COM, seiner Architektur und vorhandenen COM-Interoplösungen vertraut sein.

In diesem Lernprogramm implementieren Sie die folgenden Schnittstellendefinitionen. Diese Schnittstellen und ihre Implementierungen veranschaulichen Folgendes:

  • Marshalling und Aufheben des Marshalling über die COM/.NET-Grenze hinweg
  • Zwei unterschiedliche Ansätze für die Verwendung systemeigener COM-Objekte in .NET.
  • Ein empfohlenes Muster zum Aktivieren der benutzerdefinierten COM-Interoperabilität in .NET 5 und höher.

Der in diesem Lernprogramm verwendete Quellcode ist im Repository dotnet/samples verfügbar.

Hinweis

In .NET 8 SDK und höheren Versionen wird ein Quellgenerator bereitgestellt, um automatisch eine ComWrappers API-Implementierung für Sie zu generieren. Weitere Informationen finden Sie unter ComWrappers "Quellgenerierung".

C#-Definitionen

interface IDemoGetType
{
    string? GetString();
}

interface IDemoStoreType
{
    void StoreString(int len, string? str);
}

Win32 C++-Definitionen

MIDL_INTERFACE("92BAA992-DB5A-4ADD-977B-B22838EE91FD")
IDemoGetType : public IUnknown
{
    HRESULT STDMETHODCALLTYPE GetString(_Outptr_ wchar_t** str) = 0;
};

MIDL_INTERFACE("30619FEA-E995-41EA-8C8B-9A610D32ADCB")
IDemoStoreType : public IUnknown
{
    HRESULT STDMETHODCALLTYPE StoreString(int len, _In_z_ const wchar_t* str) = 0;
};

Übersicht über das ComWrappers Design

Die ComWrappers API wurde entwickelt, um die minimale Interaktion bereitzustellen, die für die COM-Interoperabilität mit der .NET 5+-Laufzeit erforderlich ist. Dies bedeutet, dass viele der Besonderheiten, die im integrierten COM-Interop-System vorhanden sind, nicht vorhanden sind und aus grundlegenden Bausteinen aufgebaut werden müssen. Die beiden hauptverantwortlichen Aufgaben der API sind:

  • Effiziente Objektidentifikation (z. B. Zuordnung zwischen einer IUnknown* Instanz und einem verwalteten Objekt).
  • Garbage Collector (GC)-Interaktion.

Diese Effizienzen werden erreicht, indem die Erstellung und der Erwerb von Wrappern über die ComWrappers API erfolgen.

Da die ComWrappers API so wenige Verantwortlichkeiten hat, liegt es nahe, dass die meisten Interoperabilitätsaufgaben vom Verbraucher bearbeitet werden sollten – das ist richtig. Die zusätzlichen Arbeiten sind jedoch weitgehend mechanisch und können von einer Lösung zur Quellgenerierung durchgeführt werden. Als Beispiel ist die C#/WinRT-Toolkette eine Quellgenerierungslösung, die auf ComWrappers aufbaut, um WinRT-Interop-Unterstützung bereitzustellen.

Implementieren einer ComWrappers Unterklasse

Bereitstellung einer ComWrappers-Unterklasse bedeutet, der .NET-Laufzeit genügend Informationen zur Verfügung zu stellen, um Wrapper für verwaltete Objekte zu erstellen und aufzuzeichnen, die in COM projiziert werden, sowie für COM-Objekte, die in .NET projiziert werden. Bevor wir uns eine Gliederung der Unterklasse ansehen, sollten wir einige Begriffe definieren.

Managed Object Wrapper – Verwaltete .NET-Objekte erfordern Wrapper, um die Verwendung aus einer non-.NET Umgebung zu ermöglichen. Diese Wrapper werden historisch als COM Callable Wrapper (CCW) bezeichnet.

Native Object Wrapper – COM-Objekte, die in einer non-.NET Sprache implementiert sind, erfordern Wrapper, um die Verwendung von .NET zu ermöglichen. Diese Wrapper werden in der Vergangenheit als Runtime Callable Wrapper (RCW) bezeichnet.

Schritt 1 – Definieren von Methoden zum Implementieren und Verstehen ihrer Absicht

Um den ComWrappers Typ zu erweitern, müssen Sie die folgenden drei Methoden implementieren. Jede dieser Methoden stellt die Teilnahme des Benutzers an der Erstellung oder Löschung eines Wrappers-Typs dar. Die ComputeVtables()- und CreateObject()-Methoden erstellen einen Managed Object Wrapper und einen Native Object Wrapper. Die ReleaseObjects()-Methode wird von der Runtime verwendet, um eine Anforderung für die bereitgestellte Sammlung von Wrappern zu senden, die vom zugrunde liegenden nativen Objekt „freigegeben“ wird. In den meisten Fällen kann der Text der ReleaseObjects()-Methode einfach NotImplementedException ausgeben, da er nur in einem erweiterten Szenario mit dem Verweistracker-Framework aufgerufen wird.

// See referenced sample for implementation.
class DemoComWrappers : ComWrappers
{
    protected override unsafe ComInterfaceEntry* ComputeVtables(object obj, CreateComInterfaceFlags flags, out int count) =>
        throw new NotImplementedException();

    protected override object? CreateObject(IntPtr externalComObject, CreateObjectFlags flags) =>
        throw new NotImplementedException();

    protected override void ReleaseObjects(IEnumerable objects) =>
        throw new NotImplementedException();
}

Um die ComputeVtables() Methode zu implementieren, entscheiden Sie, welche verwalteten Typen Sie unterstützen möchten. In diesem Lernprogramm unterstützen wir die beiden zuvor definierten Schnittstellen (IDemoGetType und IDemoStoreType) und einen verwalteten Typ, der die beiden Schnittstellen implementiert (DemoImpl).

class DemoImpl : IDemoGetType, IDemoStoreType
{
    string? _string;
    public string? GetString() => _string;
    public void StoreString(int _, string? str) => _string = str;
}

Für die CreateObject() Methode müssen Sie auch bestimmen, was Sie unterstützen möchten. In diesem Fall kennen wir jedoch nur die COM-Schnittstellen, an die wir interessiert sind, nicht die COM-Klassen. Die Schnittstellen, die von der COM-Seite genutzt werden, sind identisch mit den Schnittstellen, die wir von der .NET-Seite projizieren (d. h. IDemoGetType und IDemoStoreType).

In diesem Tutorial werden wir ReleaseObjects() nicht implementieren.

Schritt 2 – Implementieren ComputeVtables()

Beginnen wir mit dem Managed Object Wrapper – diese Wrapper sind einfacher. Sie erstellen für jede Schnittstelle eine Virtuelle Methodentabelle oder vtable, um sie in die COM-Umgebung zu projizieren. In diesem Lernprogramm definieren Sie eine vtable als Eine Sequenz von Zeigern, bei der jeder Zeiger eine Implementierung einer Funktion auf einer Schnittstelle darstellt – die Reihenfolge ist hier sehr wichtig. In COM erbt jede Schnittstelle von IUnknown. Der IUnknown Typ hat drei Methoden in der folgenden Reihenfolge definiert: QueryInterface(), , AddRef()und Release(). Nach den IUnknown Methoden kommen die spezifischen Schnittstellenmethoden. Ziehen Sie z. B. in Betracht IDemoGetType und IDemoStoreType. Konzeptionell würden die VTables für die Typen folgendermaßen aussehen:

IDemoGetType    | IDemoStoreType
==================================
QueryInterface  | QueryInterface
AddRef          | AddRef
Release         | Release
GetString       | StoreString

Sehen Sie sich an DemoImpl, wir haben bereits eine Implementierung für GetString() und StoreString(), aber was ist mit den IUnknown Funktionen? Die Implementierung einer IUnknown-Instanz geht über den Rahmen dieses Tutorials hinaus, kann jedoch manuell in ComWrappers ausgeführt werden. In diesem Tutorial lassen Sie die Runtime diesen Teil erledigen. Sie können die IUnknown-Implementierung mithilfe der ComWrappers.GetIUnknownImpl()-Methode abrufen.

Es mag so aussehen, als ob Sie alle Methoden implementiert haben, aber leider sind nur die IUnknown Funktionen in einer COM-vtable konsumierbar. Da COM außerhalb der Runtime liegt, müssen Sie native Funktionszeiger für Ihre DemoImpl-Implementierung erstellen. Dies kann mithilfe von C#-Funktionszeigern und dem Platzhalter UnmanagedCallersOnlyAttribute getan werden. Sie können eine Funktion erstellen, die in die vtable eingefügt werden soll, indem Sie eine static Funktion erstellen, die die COM-Funktionssignatur nachahmt. Nachfolgend sehen Sie ein Beispiel für die COM-Signatur IDemoGetType.GetString() – erinnern Sie sich daran, dass gemäß der COM-ABI das erste Argument die Instanz selbst ist.

[UnmanagedCallersOnly]
public static int GetString(IntPtr _this, IntPtr* str);

Die Wrapperimplementierung von IDemoGetType.GetString() sollte aus Marshallinglogik und anschließend einer Verteilung an das verwaltete Objekt bestehen, das umschlossen werden soll. Der gesamte Status für den Versand ist im bereitgestellten _this Argument enthalten. Das _this Argument ist tatsächlich vom Typ ComInterfaceDispatch*. Dieser Typ stellt eine Struktur auf niedriger Ebene mit einem einzelnen Feld dar, Vtabledas später erläutert wird. Weitere Details dieses Typs und seines Layouts sind ein Implementierungsdetail der Runtime und eignen sich nicht für eine verlässliche Nutzung. Verwenden Sie den folgenden Code, um die verwaltete Instanz aus einer ComInterfaceDispatch* Instanz abzurufen:

IDemoGetType inst = ComInterfaceDispatch.GetInstance<IDemoGetType>((ComInterfaceDispatch*)_this);

Nachdem Sie nun über eine C#-Methode verfügen, die in eine vtable eingefügt werden kann, können Sie die vtable erstellen. Beachten Sie die Verwendung von RuntimeHelpers.AllocateTypeAssociatedMemory() für die Zuweisung von Arbeitsspeicher auf eine Weise, die mit unloadbaren Assemblys funktioniert.

GetIUnknownImpl(
    out IntPtr fpQueryInterface,
    out IntPtr fpAddRef,
    out IntPtr fpRelease);

// Local variables with increment act as a guard against incorrect construction of
// the native vtable. It also enables a quick validation of final size.
int tableCount = 4;
int idx = 0;
var vtable = (IntPtr*)RuntimeHelpers.AllocateTypeAssociatedMemory(
    typeof(DemoComWrappers),
    IntPtr.Size * tableCount);
vtable[idx++] = fpQueryInterface;
vtable[idx++] = fpAddRef;
vtable[idx++] = fpRelease;
vtable[idx++] = (IntPtr)(delegate* unmanaged<IntPtr, IntPtr*, int>)&ABI.IDemoGetTypeManagedWrapper.GetString;
Debug.Assert(tableCount == idx);
s_IDemoGetTypeVTable = (IntPtr)vtable;

Die Zuordnung von vtables ist der erste Teil der Implementierung ComputeVtables(). Sie sollten auch umfassende COM-Definitionen für Typen erstellen, die Sie unterstützen möchten – denken Sie darüber nach, DemoImpl und welche Teile davon von COM verwendet werden können. Mithilfe der erstellten vtables können Sie jetzt eine Reihe von ComInterfaceEntry Instanzen erstellen, die die vollständige Ansicht des verwalteten Objekts in COM darstellen.

s_DemoImplDefinitionLen = 2;
int idx = 0;
var entries = (ComInterfaceEntry*)RuntimeHelpers.AllocateTypeAssociatedMemory(
    typeof(DemoComWrappers),
    sizeof(ComInterfaceEntry) * s_DemoImplDefinitionLen);
entries[idx].IID = IDemoGetType.IID_IDemoGetType;
entries[idx++].Vtable = s_IDemoGetTypeVTable;
entries[idx].IID = IDemoStoreType.IID_IDemoStoreType;
entries[idx++].Vtable = s_IDemoStoreVTable;
Debug.Assert(s_DemoImplDefinitionLen == idx);
s_DemoImplDefinition = entries;

Die Zuordnung von VTables und Einträgen für den Wrapper von verwalteten Objekten kann und sollte im Voraus erfolgen, da die Daten für alle Instanzen des Typs verwendet werden können. Die Arbeit hier kann in einem static Konstruktor oder einem Modulinitialisierer ausgeführt werden, sollte aber vorab ausgeführt werden, damit die ComputeVtables() Methode so einfach und schnell wie möglich ist.

protected override unsafe ComInterfaceEntry* ComputeVtables(object obj, CreateComInterfaceFlags flags,
out int count)
{
    if (obj is DemoImpl)
    {
        count = s_DemoImplDefinitionLen;
        return s_DemoImplDefinition;
    }

    // Unknown type
    count = 0;
    return null;
}

Nachdem Sie die ComputeVtables()-Methode implementiert haben, kann die ComWrappers-Unterklasse verwaltete Objektumhüllungen für Instanzen von DemoImpl bereitstellen. Beachten Sie, dass der zurückgegebene Managed Object Wrapper vom Aufruf GetOrCreateComInterfaceForObject() an vom Typ IUnknown*ist. Wenn die systemeigene API, die an den Wrapper übergeben wird, eine andere Schnittstelle erfordert, muss eine Marshal.QueryInterface() für diese Schnittstelle ausgeführt werden.

var cw = new DemoComWrappers();
var demo = new DemoImpl();
IntPtr ccw = cw.GetOrCreateComInterfaceForObject(demo, CreateComInterfaceFlags.None);

Schritt 3 – Implementieren CreateObject()

Das Erstellen eines nativen Objektwrappers verfügt über mehr Implementierungsoptionen und viel mehr Nuancen als das Erstellen eines Managed Object Wrappers. Die erste zu behandelnde Frage ist, wie permissiv die ComWrappers Unterklasse bei der Unterstützung von COM-Typen sein wird. Um alle COM-Typen zu unterstützen, was möglich ist, müssen Sie eine erhebliche Menge an Code schreiben oder einige kreative Anwendungen von Reflection.Emit nutzen. In diesem Lernprogramm unterstützen Sie nur COM-Instanzen, die sowohl IDemoGetType als auch IDemoStoreType implementieren. Da Sie wissen, dass es einen endlichen Satz gibt und eingeschränkt ist, dass jede bereitgestellte COM-Instanz beide Schnittstellen implementieren muss, können Sie einen einzelnen, statisch definierten Wrapper bereitstellen. Dynamische Fälle sind jedoch häufig genug in COM vorhanden, um beide Optionen zu untersuchen.

Wrapper für statische native Objekte

Sehen wir uns zuerst die statische Implementierung an. Der statische native Objektwrapper umfasst das Definieren eines verwalteten Typs, der die .NET-Schnittstellen implementiert und die Aufrufe des verwalteten Typs an die COM-Instanz weiterleiten kann. Es folgt eine grobe Erläuterung des statischen Wrappers.

// See referenced sample for implementation.
class DemoNativeStaticWrapper
    : IDemoGetType
    , IDemoStoreType
{
    public string? GetString() =>
        throw new NotImplementedException();

    public void StoreString(int len, string? str) =>
        throw new NotImplementedException();
}

Um eine Instanz dieser Klasse zu erstellen und als Wrapper bereitzustellen, müssen Sie einige Richtlinien definieren. Wenn dieser Typ als Wrapper verwendet wird, müsste es so sein, dass die zugrunde liegende COM-Instanz ebenfalls beide Schnittstellen implementiert. Wenn Sie diese Richtlinie übernehmen, müssen Sie dies durch Aufrufe von Marshal.QueryInterface() in der COM-Instanz bestätigen.

int hr = Marshal.QueryInterface(ptr, ref IDemoGetType.IID_IDemoGetType, out IntPtr IDemoGetTypeInst);
if (hr != 0)
{
    return null;
}

hr = Marshal.QueryInterface(ptr, ref IDemoStoreType.IID_IDemoStoreType, out IntPtr IDemoStoreTypeInst);
if (hr != 0)
{
    Marshal.Release(IDemoGetTypeInst);
    return null;
}

return new DemoNativeStaticWrapper()
{
    IDemoGetTypeInst = IDemoGetTypeInst,
    IDemoStoreTypeInst = IDemoStoreTypeInst
};

Wrapper für dynamische native Objekte

Dynamische Wrapper sind flexibler, da sie es ermöglichen, Typen zur Laufzeit anstatt statisch abzufragen. Um diesen Support bereitzustellen, verwenden Sie IDynamicInterfaceCastable. Beachten Sie, dass DemoNativeDynamicWrapper nur diese Schnittstelle implementiert. Die von der Schnittstelle bereitgestellte Funktionalität ist die Möglichkeit, zu bestimmen, welcher Typ zur Laufzeit unterstützt wird. Die Quelle für dieses Tutorial führt während der Erstellung eine statische Überprüfung durch, aber das ist einfach für die gemeinsame Nutzung des Codes, da die Überprüfung bis zu einem Aufruf von DemoNativeDynamicWrapper.IsInterfaceImplemented() zurückgestellt werden kann.

// See referenced sample for implementation.
internal class DemoNativeDynamicWrapper
    : IDynamicInterfaceCastable
{
    public RuntimeTypeHandle GetInterfaceImplementation(RuntimeTypeHandle interfaceType) =>
        throw new NotImplementedException();

    public bool IsInterfaceImplemented(RuntimeTypeHandle interfaceType, bool throwIfNotImplemented) =>
        throw new NotImplementedException();
}

Sehen wir uns eine der Schnittstellen an, die von DemoNativeDynamicWrapper dynamisch unterstützt wird. Der folgende Code stellt die Implementierung von IDemoStoreType mit dem Feature Standardschnittstellenmethoden bereit.

[DynamicInterfaceCastableImplementation]
unsafe interface IDemoStoreTypeNativeWrapper : IDemoStoreType
{
    public static void StoreString(IntPtr inst, int len, string? str);

    void IDemoStoreType.StoreString(int len, string? str)
    {
        var inst = ((DemoNativeDynamicWrapper)this).IDemoStoreTypeInst;
        StoreString(inst, len, str);
    }
}

In diesem Beispiel sind zwei wichtige Punkte zu beachten:

  1. Das DynamicInterfaceCastableImplementationAttribute -Attribut. Dieses Attribut ist für jeden Typ erforderlich, der von einer IDynamicInterfaceCastable Methode zurückgegeben wird. Es hat den zusätzlichen Vorteil, IL Trimming einfacher zu gestalten, was bedeutet, dass AOT-Szenarien zuverlässiger sind.
  2. Die Umwandlung in DemoNativeDynamicWrapper. Dies ist Teil der dynamischen Natur von IDynamicInterfaceCastable. Der Typ, der von IDynamicInterfaceCastable.GetInterfaceImplementation() zurückgegeben wird, wird verwendet, um den Typ abzudecken, der IDynamicInterfaceCastable implementiert. Der Hauptpunkt hier ist, dass der this Zeiger nicht das ist, was er vorgibt zu sein, weil wir einen Fall von DemoNativeDynamicWrapper zu IDemoStoreTypeNativeWrapper zulassen.

Weiterleiten von Aufrufen an die COM-Instanz

Unabhängig davon, welcher native Objektwrapper verwendet wird, benötigen Sie die Möglichkeit, Funktionen in einer COM-Instanz aufzurufen. Die Implementierung von IDemoStoreTypeNativeWrapper.StoreString() kann als Beispiel für die Verwendung von unmanaged C#-Funktionszeigern dienen.

public static void StoreString(IntPtr inst, int len, string? str)
{
    IntPtr strLocal = Marshal.StringToCoTaskMemUni(str);
    int hr = ((delegate* unmanaged<IntPtr, int, IntPtr, int>)(*(*(void***)inst + 3 /* IDemoStoreType.StoreString slot */)))(inst, len, strLocal);
    if (hr != 0)
    {
        Marshal.FreeCoTaskMem(strLocal);
        Marshal.ThrowExceptionForHR(hr);
    }
}

Untersuchen wir die Dereferenzierung der COM-Instanz für den Zugriff auf die VTable-Implementierung. Die COM-ABI definiert, dass der erste Zeiger eines Objekts auf die V-Tabelle des Typs verweist und von dort aus auf den gewünschten Steckplatz zugegriffen werden kann. Angenommen, die Adresse des COM-Objekts ist 0x10000. Der erste Wert der Zeigergröße sollte die Adresse der vtable sein – in diesem Beispiel 0x20000. Sobald Sie sich auf der vtable befinden, suchen Sie nach dem vierten Steckplatz (Index 3 in nullbasierter Indizierung), um auf die StoreString() Implementierung zuzugreifen.

COM instance
0x10000  0x20000

VTable for IDemoStoreType
0x20000  <Address of QueryInterface>
0x20008  <Address of AddRef>
0x20010  <Address of Release>
0x20018  <Address of StoreString>

Mit dem Funktionszeiger können Sie dann die Memberfunktion des Objekts aufrufen, indem Sie die Objektinstanz als ersten Parameter übergeben. Dieses Muster sollte Ihnen vertraut vorkommen, basierend auf den Funktionsdefinitionen der Implementierung des Managed Object Wrappers.

Sobald die CreateObject() Methode implementiert wurde, kann die ComWrappers Unterklasse native Objektwrapper für COM-Instanzen erstellen, die beide IDemoGetType implementieren und IDemoStoreType.

IntPtr iunk = ...; // Get a COM instance from native code.
object rcw = cw.GetOrCreateObjectForComInstance(iunk, CreateObjectFlags.UniqueInstance);

Schritt 4: Details zur Lebensdauer des Wrappers für native Objekte

Die ComputeVtables()- und CreateObject()-Implementierungen behandelten einige Details zur Wrapper-Lebensdauer, aber es gibt weitere Überlegungen. Dies kann zwar ein kurzer Schritt sein, kann aber auch die Komplexität des ComWrappers Designs erheblich erhöhen.

Im Gegensatz zum Managed Object Wrapper, der durch Aufrufe seiner AddRef() und Release() Methoden gesteuert wird, wird die Lebensdauer eines nativen Objektwrappers nicht deterministisch von der GC behandelt. Die Frage hier ist, wann der Wrapper für native Objekte Release() in IntPtr aufruft, was die COM-Instanz darstellt. Es gibt zwei allgemeine Buckets:

  1. Der Finalizer des nativen Objektwrappers ist für das Aufrufen der COM-Instanzmethode Release() verantwortlich. Dies ist der einzige Zeitpunkt, zu dem es sicher ist, diese Methode aufzurufen. An diesem Punkt wurde es von der GC korrekt bestimmt, dass in der .NET-Laufzeit keine anderen Verweise auf den Native Object Wrapper vorhanden sind. Es kann hier Komplexität geben, wenn Sie com Apartments richtig unterstützen; weitere Informationen finden Sie im Abschnitt "Zusätzliche Überlegungen ".

  2. Der Native Object Wrapper implementiert IDisposable und ruft Release() in Dispose() auf.

Hinweis

Das IDisposable-Muster sollte nur unterstützt werden, wenn das CreateObject()-Flag während des CreateObjectFlags.UniqueInstance-Aufrufs übergeben wurde. Wenn diese Anforderung nicht befolgt wird, kann es passieren, dass verworfene Native Objekt-Wrapper nach ihrer Entsorgung wiederverwendet werden.

Verwenden der ComWrappers Unterklasse

Sie verfügen jetzt über eine ComWrappers Unterklasse, die getestet werden kann. Um zu vermeiden, dass eine systemeigene Bibliothek erstellt wird, die eine COM-Instanz zurückgibt, die IDemoGetType und IDemoStoreType implementiert, verwenden Sie den Managed Object Wrapper und behandeln Sie ihn als COM-Instanz – dies muss möglich sein, um ihn trotzdem an COM zu übergeben.

Erstellen wir zuerst einen managed object wrapper. Instanziieren Sie eine DemoImpl Instanz, und zeigen Sie den aktuellen Zeichenfolgenstatus an.

var demo = new DemoImpl();

string? value = demo.GetString();
Console.WriteLine($"Initial string: {value ?? "<null>"}");

Jetzt können Sie eine Instanz von DemoComWrappers und einen Managed Object Wrapper erstellen, den Sie dann in eine COM-Umgebung übergeben können.

var cw = new DemoComWrappers();

IntPtr ccw = cw.GetOrCreateComInterfaceForObject(demo, CreateComInterfaceFlags.None);

Anstatt den Managed Object Wrapper an eine COM-Umgebung zu übergeben, geben Sie vor, dass Sie diese COM-Instanz gerade erhalten haben, sodass Sie stattdessen einen nativen Objektwrapper dafür erstellen.

var rcw = cw.GetOrCreateObjectForComInstance(ccw, CreateObjectFlags.UniqueInstance);

Mit dem nativen Objektwrapper sollten Sie es in eine der gewünschten Schnittstellen umwandeln und als normales verwaltetes Objekt verwenden können. Sie können die DemoImpl Instanz untersuchen und die Auswirkungen von Vorgängen auf den nativen Objektwrapper beobachten, der einen verwalteten Objektwrapper umschließt, der wiederum die verwaltete Instanz umschließt.

var getter = (IDemoGetType)rcw;
var store = (IDemoStoreType)rcw;

string msg = "hello world!";
store.StoreString(msg.Length, msg);
Console.WriteLine($"Setting string through wrapper: {msg}");

value = demo.GetString();
Console.WriteLine($"Get string through managed object: {value}");

msg = msg.ToUpper();
demo.StoreString(msg.Length, msg.ToUpper());
Console.WriteLine($"Setting string through managed object: {msg}");

value = getter.GetString();
Console.WriteLine($"Get string through wrapper: {value}");

Da Ihre ComWrapper-Unterklasse für die Unterstützung von CreateObjectFlags.UniqueInstance konzipiert wurde, können Sie den Wrapper für native Objekte sofort bereinigen, anstatt auf eine GC zu warten.

(rcw as IDisposable)?.Dispose();

COM-Aktivierung mit ComWrappers

Die Erstellung von COM-Objekten erfolgt in der Regel über die COM-Aktivierung – ein komplexes Szenario außerhalb des Umfangs dieses Dokuments. Um sich an ein konzeptionelles Muster zu orientieren, führen wir die CoCreateInstance() API ein, die zur COM-Aktivierung verwendet wird, und veranschaulichen, wie sie mit ComWrappers verwendet werden kann.

Angenommen, Sie haben den folgenden C#-Code in Ihrer Anwendung. Im folgenden Beispiel wird CoCreateInstance() verwendet, um eine COM-Klasse zu aktivieren und das integrierte COM-Interoperabilitätssystem zu nutzen, um die COM-Instanz an die entsprechende Schnittstelle zu übergeben. Beachten Sie, dass die Verwendung von typeof(I).GUID auf eine Assertion beschränkt ist und ein Fall für die Verwendung von Reflexion ist, was Auswirkungen haben kann, wenn der Code AOT-freundlich ist.

public static I ActivateClass<I>(Guid clsid, Guid iid)
{
    Debug.Assert(iid == typeof(I).GUID);
    int hr = CoCreateInstance(ref clsid, IntPtr.Zero, /*CLSCTX_INPROC_SERVER*/ 1, ref iid, out object obj);
    if (hr < 0)
    {
        Marshal.ThrowExceptionForHR(hr);
    }
    return (I)obj;
}

[DllImport("Ole32")]
private static extern int CoCreateInstance(
    ref Guid rclsid,
    IntPtr pUnkOuter,
    int dwClsContext,
    ref Guid riid,
    [MarshalAs(UnmanagedType.Interface)] out object ppObj);

Das Übertragen des oben Genannten auf die Verwendung von ComWrappers umfasst das Entfernen der MarshalAs(UnmanagedType.Interface) aus dem P/Invoke CoCreateInstance() und die manuelle Durchführung des Marshallings.

static ComWrappers s_ComWrappers = ...;

public static I ActivateClass<I>(Guid clsid, Guid iid)
{
    Debug.Assert(iid == typeof(I).GUID);
    int hr = CoCreateInstance(ref clsid, IntPtr.Zero, /*CLSCTX_INPROC_SERVER*/ 1, ref iid, out IntPtr obj);
    if (hr < 0)
    {
        Marshal.ThrowExceptionForHR(hr);
    }
    return (I)s_ComWrappers.GetOrCreateObjectForComInstance(obj, CreateObjectFlags.None);
}

[DllImport("Ole32")]
private static extern int CoCreateInstance(
    ref Guid rclsid,
    IntPtr pUnkOuter,
    int dwClsContext,
    ref Guid riid,
    out IntPtr ppObj);

Es ist auch möglich, fabrikartige Funktionen wie ActivateClass<I> zu abstrahieren, indem man die Aktivierungslogik in den Klassenkonstruktor für einen nativen Objekt Wrapper einfügt. Der Konstruktor kann die ComWrappers.GetOrRegisterObjectForComInstance() API verwenden, um das neu erstellte verwaltete Objekt der aktivierten COM-Instanz zuzuordnen.

Weitere Überlegungen

Native AOT – Die AOT-Kompilierung (Ahead-of-Time) bietet verbesserte Startkosten, da JIT-Kompilierung vermieden wird. Das Entfernen der Notwendigkeit der JIT-Kompilierung ist häufig auch auf einigen Plattformen erforderlich. Die Unterstützung von AOT war ein Ziel der ComWrappers API, aber jede Wrapperimplementierung muss vorsichtig sein, nicht versehentlich Fälle einzuführen, in denen AOT fehlschlägt, z. B. die Verwendung von Reflexion. Die Type.GUID Eigenschaft ist ein Beispiel dafür, wo Spiegelung verwendet wird, aber auf nicht offensichtliche Weise. Die Type.GUID-Eigenschaft verwendet Reflexion, um die Attribute des Typs und dann möglicherweise den Namen des Typs und die enthaltene Assembly zu überprüfen und den Wert zu generieren.

Quellgenerierung – Der Großteil des Codes, der für die COM-Interoperabilität erforderlich ist, und eine ComWrappers Implementierung kann wahrscheinlich von einigen Tools automatisch generiert werden. Die Quelle für beide Wrappertypen kann anhand der richtigen COM-Definitionen generiert werden, z. B. Typbibliothek (TLB), IDL oder eine primäre Interopassembly (PIA).

Globale Registrierung – Da die ComWrappers API als neue Phase der COM-Interoperabilität konzipiert wurde, musste sie teilweise in das vorhandene System integriert werden. Es gibt statische Methoden in der ComWrappers API mit globaler Auswirkung, die die Registrierung einer globalen Instanz für verschiedene Unterstützungsmöglichkeiten ermöglichen. Diese Methoden sind für ComWrappers-Instanzen konzipiert, die in allen Fällen eine umfassende COM-Interop-Unterstützung erwarten – ähnlich dem integrierten COM-Interop-System.

Unterstützung von Reference Tracker – Diese Unterstützung wird primär für WinRT-Szenarien verwendet und stellt ein erweitertes Szenario dar. Bei den meisten ComWrapper-Implementierungen sollte ein CreateComInterfaceFlags.TrackerSupport- oder CreateObjectFlags.TrackerObject-Flag eine NotSupportedException ausgeben. Wenn Sie diese Unterstützung aktivieren möchten, z. B. auf einer Windows- oder sogar nicht-Windows-Plattform, wird dringend empfohlen, auf die C#/WinRT-Toolkette zu verweisen.

Abgesehen von der Lebensdauer, dem Typsystem und den funktionalen Features, die zuvor erläutert werden, erfordert eine COM-kompatible Implementierung ComWrappers zusätzliche Überlegungen. Für jede Implementierung, die auf der Windows-Plattform verwendet wird, gibt es die folgenden Überlegungen:

  • Apartments – Die Organisationsstruktur der COM für Threading heißt "Apartments" und hat strenge Regeln, die für stabile Vorgänge befolgt werden müssen. In diesem Tutorial werden keine apartmentbasierten Wrapper für native Objekte implementiert, aber jede produktionsbereite Implementierung sollte apartmentbasiert sein. Dazu empfehlen wir die Verwendung der RoGetAgileReference in Windows 8 eingeführten API. Berücksichtigen Sie für Versionen vor Windows 8 die globale Schnittstellentabelle.

  • Sicherheit – COM bietet ein umfassendes Sicherheitsmodell für die Klassenaktivierung und die Proxied-Berechtigung.