Teilen über


Tutorial: Verwenden der ComWrappers-API

In diesem Tutorial erfahren Sie, wie Sie den ComWrappers-Typ ordnungsgemäß als Unterklasse festlegen, um eine optimierte und AOT-freundliche COM-Interoplösung bereitzustellen. Bevor Sie mit diesem Tutorial beginnen, sollten Sie mit COM, der Architektur und vorhandenen COM-Interoplösungen vertraut sein.

In diesem Tutorial 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 Nutzung nativer COM-Objekte in .NET
  • Ein empfohlenes Muster zum Aktivieren der benutzerdefinierten COM-Interop in .NET 5 und höher

Der gesamte Quellcode, der in diesem Tutorial verwendet wird, ist im dotnet/samples-Repository 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 ComWrappersQuellgenerierung.

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 eine minimale Interaktion bereitzustellen, die zum Erreichen der COM-Interop mit der .NET 5+-Runtime 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 Hauptaufgaben der API sind:

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

Diese Effizienz wird erreicht, indem die Erstellung und Übernahme des Wrappers über die ComWrappers-API durchgeführt wird.

Da die ComWrappers-API so wenige Zuständigkeiten hat, liegt es nah, dass der Großteil der Interop-Arbeit vom Consumer erledigt werden sollte – dies ist richtig. Die zusätzlichen Arbeiten sind jedoch weitgehend automatischer Natur und können durch eine Quellgenerierungslösung ausgeführt werden. Die C#/WinRT-Toolkette ist beispielsweise eine Lösung zur Quellgenerierung, die auf ComWrappers aufsetzt, um winRT-Interopunterstützung zu bieten.

Implementieren einer ComWrappers-Unterklasse

Das Bereitstellen einer ComWrappers-Unterklasse bedeutet, dass der .NET-Runtime genügend Informationen bereitgestellt werden, sodass sie Wrapper für verwaltete Objekte erstellen und aufzeichnen kann, die in COM-Objekte projiziert werden, und für COM-Objekte, die in .NET projiziert werden. Bevor wir uns eine Gliederung der Unterklasse ansehen, sollten wir einige Begriffe definieren.

Wrapper für verwaltete Objekte: Verwaltete .NET-Objekte erfordern Wrapper, um die Verwendung in einer Nicht-.NET-Umgebung zu ermöglichen. Diese Wrapper wurden in der Vergangenheit als COM Callable Wrappers (CCW) bezeichnet.

Nativer Objektwrapper: COM-Objekte, die in einer Nicht-.NET-Sprache implementiert sind, erfordern Wrapper, um die Verwendung in .NET zu ermöglichen. Diese Wrapper wurden in der Vergangenheit als Runtime Callable Wrappers (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 von Benutzer*innen an der Erstellung oder Löschung eines Wrappertyps dar. Die ComputeVtables()-Methode und die CreateObject()-Methode erstellen einen Wrapper für verwaltete Objekte bzw. einen Wrapper für statische Objekte. 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, müssen Sie entscheiden, welche verwalteten Typen Sie unterstützen möchten. In diesem Tutorial behandeln 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, die uns interessieren, nicht die COM-Klassen. Die Schnittstellen, die von der COM-Seite genutzt werden, sind identisch mit denen, die wir von der .NET-Seite projizieren (d. h. IDemoGetType und IDemoStoreType).

Wir implementieren ReleaseObjects() nicht in diesem Tutorial.

Schritt 2: Implementieren von ComputeVtables()

Beginnen wir mit dem Wrapper für verwaltete Objekte – diese Wrapper sind einfacher. Sie erstellen eine Virtual Method Table oder vtable für jede Schnittstelle, um sie in die COM-Umgebung zu projizieren. In diesem Tutorial definieren Sie eine VTable als eine Sequenz von Zeigern, wobei jeder Zeiger eine Implementierung einer Funktion in einer Schnittstelle darstellt. Die Reihenfolge ist hier sehr wichtig. In COM erbt jede Schnittstelle von IUnknown. Der IUnknown-Typ verfügt über drei Methoden, die in der folgenden Reihenfolge definiert sind: QueryInterface(), AddRef() und Release(). Nach den IUnknown-Methoden kommen die spezifischen Schnittstellenmethoden. Betrachten Sie beispielsweise IDemoGetType und IDemoStoreType. Vom Konzept her würden die VTables für die Typen wie folgt aussehen:

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

Wenn wir uns DemoImpl ansehen, verfügen wir bereits über 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 scheint, als ob Sie alle Methoden implementiert haben, aber leider sind nur die IUnknown-Funktionen in einer COM-VTable nutzbar. 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 UnmanagedCallersOnlyAttribute erfolgen. Sie können eine Funktion erstellen, die in die VTable eingefügt werden soll, indem Sie eine static-Funktion erstellen, die die COM-Funktionssignatur imitiert. Im Folgenden finden Sie ein Beispiel für die COM-Signatur für IDemoGetType.GetString() – bei der COM-ABI erinnern Sie sich vielleicht noch, dass 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 Zustand für die Verteilung ist im bereitgestellten _this-Argument enthalten. Das _this-Argument ist vom Typ ComInterfaceDispatch*. Dieser Typ stellt eine Struktur auf niedriger Ebene mit dem einzelnen Feld Vtable dar, das 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 in einer Weise, die mit nicht entladbaren 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 von ComputeVtables(). Sie sollten auch umfassende COM-Definitionen für Typen erstellen, die Sie unterstützen möchten – denken Sie an DemoImpl und welche Teile davon von COM verwendet werden sollten. 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, aber sie sollte im Voraus 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 Wrapper für verwaltete Objekte für Instanzen von DemoImpl erstellen. Beachten Sie, dass der zurückgegebene Wrapper für verwaltete Objekte aus dem Aufruf von GetOrCreateComInterfaceForObject() vom Typ IUnknown* ist. Wenn die native 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 von CreateObject()

Das Erstellen eines Wrappers für native Objekte hat mehr Implementierungsoptionen und viel mehr Nuancen als das Erstellen eines Wrappers für verwaltete Objekte. Die erste zu beantwortende Frage ist, wie zulässig die ComWrappers-Unterklasse in unterstützenden COM-Typen sein wird. Um alle COM-Typen zu unterstützen, was möglich ist, müssen Sie eine beträchtliche Menge an Code schreiben oder Reflection.Emit clever verwenden. In diesem Tutorial unterstützen Sie nur COM-Instanzen, die sowohl IDemoGetType als auch IDemoStoreType implementieren. Da Sie wissen, dass eine endliche Anzahl vorhanden ist und dass alle bereitgestellten COM-Instanzen beide Schnittstellen implementieren müssen, können Sie einen einzelnen statisch definierten Wrapper bereitstellen. Dynamische Fälle sind jedoch in COM so häufig, dass wir beide Optionen untersuchen.

Wrapper für statische native Objekte

Sehen wir uns zuerst die statische Implementierung an. Der Wrapper für statische native Objekte umfasst die Definition 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 eine Richtlinie definieren. Wenn dieser Typ als Wrapper verwendet wird, liegt es nahe, dass die zugrunde liegende COM-Instanz beide Schnittstellen implementieren soll, da der Wrapper 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 eine Möglichkeit für die Abfrage von Typen zur Laufzeit statt einer statischen Abfrage bieten. Um diese Unterstützung zu bieten, nutzen Sie IDynamicInterfaceCastable – weitere Details finden Sie hier. Beachten Sie, dass DemoNativeDynamicWrapper nur diese Schnittstelle implementiert. Die von der Schnittstelle bereitgestellte Funktionalität bietet eine 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 dies dient einfach der Codefreigabe, da die Überprüfung auch später durchgeführt werden kann, bis ein Aufruf von DemoNativeDynamicWrapper.IsInterfaceImplemented() erfolgt.

// 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 DemoNativeDynamicWrapper dynamisch unterstützt. 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, das Kürzen von IL zu vereinfachen, was bedeutet, dass AOT-Szenarios zuverlässiger sind.
  2. Die Umwandlung in DemoNativeDynamicWrapper. Dies ist Teil der dynamischen Eigenschaft von IDynamicInterfaceCastable. Der Typ, der von IDynamicInterfaceCastable.GetInterfaceImplementation() zurückgegeben wird, wird verwendet, um den Typ zu „überdecken“, der IDynamicInterfaceCastable implementiert. Wichtig ist hier: Der this-Zeiger ist nicht das, was er vorgibt zu sein, da wir einen Case von DemoNativeDynamicWrapper zu IDemoStoreTypeNativeWrapper erlauben.

Weiterleiten von Aufrufen an die COM-Instanz

Unabhängig davon, welcher Wrapper für native Objekte 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 VTable des Typs verweist und von dort aus auf den gewünschten Slot zugegriffen werden kann. Angenommen, die Adresse des COM-Objekts lautet 0x10000. Der erste Wert in Zeigergröße sollte die Adresse der VTable sein – in diesem Beispiel 0x20000. Sobald Sie sich bei der VTable befinden, suchen Sie nach dem vierten Slot (Index 3 bei 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 an diese Memberfunktion für dieses Objekt senden, indem Sie die Objektinstanz als ersten Parameter übergeben. Dieses Muster sollte anhand der Funktionsdefinitionen der Implementierung des Wrappers für verwaltete Objekte vertraut sein.

Sobald die CreateObject()-Methode implementiert wurde, kann die ComWrappers-Unterklasse Wrapper für native Objekte für COM-Instanzen erstellen, die sowohl IDemoGetType als auch IDemoStoreType implementieren.

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 decken einige Details zur Wrapperlebensdauer ab, es gibt jedoch weitere Überlegungen. Dies kann zwar schnell gehen, aber auch die Komplexität des ComWrappers-Designs erheblich erhöhen.

Im Gegensatz zum Wrapper für verwaltete Objekte, der durch Aufrufe seiner AddRef()- und Release()-Methoden gesteuert wird, wird die Lebensdauer eines Wrappers für native Objekte 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 Wrappers für native Objekte ist für das Aufrufen der Release()-Methode der COM-Instanz verantwortlich. Dies ist der einzige Zeitpunkt, zu dem es sicher ist, diese Methode aufzurufen. An diesem Punkt wurde von der GC korrekt ermittelt, dass es keine weiteren Verweise auf den Wrapper für native Objekte in der .NET-Runtime gibt. Hier kann es zu Komplexitäten kommen, wenn Sie COM-Apartments ordnungsgemäß unterstützen. Weitere Informationen finden Sie im Abschnitt Zusätzliche Überlegungen.

  2. Der Wrapper für native Objekte implementiert IDisposable und ruft Release() in Dispose() auf.

Hinweis

Das IDisposable-Muster sollte nur unterstützt werden, wenn das CreateObjectFlags.UniqueInstance-Flag während des CreateObject()-Aufrufs übergeben wurde. Wenn diese Anforderung nicht erfüllt wird, ist es möglich, dass verworfene Wrapper für native Objekte wiederverwendet werden, nachdem sie entsorgt wurden.

Verwenden der ComWrappers-Unterklasse

Sie verfügen jetzt über eine ComWrappers-Unterklasse, die getestet werden kann. Um das Erstellen einer nativen Bibliothek zu vermeiden, die eine COM-Instanz zurückgibt, die IDemoGetType und IDemoStoreType implementiert, verwenden Sie den Wrapper für verwaltete Objekte und behandeln ihn als COM-Instanz. Dies muss möglich sein, um ihn trotzdem COM zu übergeben.

Erstellen wir zuerst einen Wrapper für verwaltete Objekte. Instanziieren Sie eine DemoImpl-Instanz, und zeigen Sie den aktuellen Zeichenfolgenzustand 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 Wrapper für verwaltete Objekte erstellen, die Sie dann an eine COM-Umgebung übergeben können.

var cw = new DemoComWrappers();

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

Anstatt den Wrapper für verwaltete Objekte an eine COM-Umgebung zu übergeben, tun Sie so, als hätten Sie gerade diese COM-Instanz erhalten, sodass Sie stattdessen einen Wrapper für native Objekte dafür erstellen.

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

Den Wrapper für native Objekte sollten Sie in eine der gewünschten Schnittstellen umwandeln und als normales verwaltetes Objekt verwenden können. Sie können die DemoImpl-Instanz untersuchen und die Auswirkung von Vorgängen auf den Wrapper für native Objekte beobachten, der einen Wrapper für verwaltete Objekte 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, das über den Rahmen dieses Dokuments hinausgeht. Um ein zu befolgendes konzeptionelles Muster bereitzustellen, führen wir die API CoCreateInstance() ein, die für die 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 und das integrierte COM-Interopsystem zu aktivieren und die COM-Instanz an die entsprechende Schnittstelle zu marshallen. 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, Funktionen im Factory-Stil abstrahieren, z. B. ActivateClass<I>, durch Einschließen der Aktivierungslogik in den Klassenkonstruktor für einen Wrapper für native Objekte. Der Konstruktor kann die ComWrappers.GetOrRegisterObjectForComInstance()-API verwenden, um das neu erstellte verwaltete Objekt der aktivierten COM-Instanz zuzuordnen.

Weitere Überlegungen

Native AOT: Die Ahead-of-time (AOT)-Kompilierung bedeutet verbesserte Startupkosten, da die JIT-Kompilierung vermieden wird. Die Notwendigkeit, die JIT-Kompilierung zu entfernen, ist auch häufig auf einigen Plattformen erforderlich. Die Unterstützung von AOT war ein Ziel der ComWrappers-API, aber bei jeder Wrapperimplementierung muss darauf geachtet werden, dass nicht versehentlich Fälle entstehen, bei denen AOT abstürzt, z. B. bei der Verwendung der Reflexion. Die Type.GUID-Eigenschaft ist ein Beispiel für die Verwendung von Reflexion, aber auf eine 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 größte Teil des Codes, der für COM-Interop und eine ComWrappers-Implementierung benötigt wird, kann wahrscheinlich von einigen Tools automatisch generiert werden. Die Quelle für beide Wrappertypen kann mit den richtigen COM-Definitionen generiert werden, z. B. Typbibliothek (TLB), IDL oder eine primäre Interop-Assembly (PIA).

Globale Registrierung: Da die ComWrappers-API als neue Phase der COM-Interop konzipiert wurde, musste die Möglichkeit bestehen, sie teilweise mit dem vorhandenen System zu integrieren. Es gibt global wirkende statische Methoden für die ComWrappers-API, die die Registrierung einer globalen Instanz für verschiedene Unterstützungsmöglichkeiten erlauben. 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.

Verweistrackerunterstützung: Diese Unterstützung wird hauptsächlich für WinRT-Szenarios 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 wurden, erfordert eine COM-konforme Implementierung von ComWrappers zusätzliche Überlegungen. Für jede Implementierung, die auf der Windows-Plattform verwendet wird, gibt es die folgenden Überlegungen:

  • Apartments: Die Organisationsstruktur von COM für Threading heißt „Apartments“ und weist strenge Regeln auf, die für stabile Vorgänge eingehalten werden müssen. In diesem Tutorial werden keine apartmentbasierten Wrapper für native Objekte implementiert, aber jede produktionsbereite Implementierung sollte apartmentbasiert sein. Hierzu wird empfohlen, die in Windows 8 eingeführte RoGetAgileReference-API zu verwenden. Für Versionen vor Windows 8 sollten Sie die globale Schnittstellentabelle in Betracht ziehen.

  • Sicherheit: COM bietet ein umfassendes Sicherheitsmodell für die Klassenaktivierung und Proxyberechtigung.