Partager via


Tutoriel : Utiliser l’API ComWrappers

Dans ce tutoriel, vous allez apprendre à sous-classer correctement le ComWrappers type pour fournir une solution COM interop optimisée et conviviale AOT. Avant de commencer ce tutoriel, vous devez être familiarisé avec COM, son architecture et les solutions COM Interop existantes.

Dans ce tutoriel, vous allez implémenter les définitions d’interface suivantes. Ces interfaces et leurs implémentations illustrent :

  • Conversion et inversion des types à travers la frontière COM/.NET.
  • Deux approches distinctes pour consommer des objets COM natifs dans .NET.
  • Modèle recommandé pour activer l’interopérabilité COM personnalisée dans .NET 5 et au-delà.

Tout le code source utilisé dans ce tutoriel est disponible dans le référentiel dotnet/samples.

Remarque

Dans le Kit de développement logiciel (SDK) .NET 8 et les versions ultérieures, un générateur source est fourni pour générer automatiquement une implémentation d’API ComWrappers pour vous. Pour plus d’informations, consultez ComWrappers la génération de la source.

Définitions C#

interface IDemoGetType
{
    string? GetString();
}

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

Définitions Win32 C++

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;
};

Vue d’ensemble de la ComWrappers conception

L’API ComWrappers a été conçue pour fournir l’interaction minimale nécessaire pour effectuer l’interopérabilité COM avec le runtime .NET 5+. Cela signifie que de nombreuses facilités qui existent avec le système d'interopérabilité COM intégré ne sont pas présents et doivent être construites à partir d'éléments de base. Les deux principales responsabilités de l’API sont les suivantes :

  • Identification efficace des objets (par exemple, mappage entre une IUnknown* instance et un objet managé).
  • Interaction avec le collecteur de déchets (GC).

Ces gains d’efficacité sont réalisés en exigeant la création et l’acquisition de wrapper par le biais de l’API ComWrappers .

Étant donné que l’API ComWrappers a si peu de responsabilités, il est logique que la plupart des travaux d'interopérabilité soient gérés par le consommateur, et c'est vrai. Toutefois, le travail supplémentaire est en grande partie mécanique et peut être effectué par une solution de génération source. Par exemple, la chaîne d’outils C#/WinRT est une solution de génération de source construite sur ComWrappers pour fournir le support d’interopérabilité WinRT.

Implémenter une ComWrappers sous-classe

Fournir une ComWrappers sous-classe signifie fournir suffisamment d’informations au runtime .NET pour créer et enregistrer des wrappers pour les objets managés projetés dans COM et les objets COM projetés dans .NET. Avant d’examiner un plan de la sous-classe, nous devons définir certains termes.

Wrapper d’objets managés : les objets .NET managés nécessitent des wrappers pour activer l’utilisation à partir d’un environnement non-.NET. Ces wrappers sont historiquement appelés enveloppes appelables COM (CCW).

Wrapper d’objet natif : les objets COM implémentés dans un langage non-.NET nécessitent des wrappers pour permettre leur utilisation depuis .NET. Ces wrappers sont historiquement appelés Runtime Callable Wrappers (RCW).

Étape 1 : définir des méthodes pour implémenter et comprendre leur intention

Pour étendre le ComWrappers type, vous devez implémenter les trois méthodes suivantes. Chacune de ces méthodes représente la participation de l’utilisateur à la création ou à la suppression d’un type de wrapper. Les méthodes ComputeVtables() et CreateObject() créent respectivement un Wrapper d'Objet Géré et un Wrapper d'Objet Natif. La ReleaseObjects() méthode est utilisée par le runtime pour effectuer une demande pour que la collection fournie de wrappers soit « libérée » de l’objet natif sous-jacent. Dans la plupart des cas, le corps de la ReleaseObjects() méthode peut simplement lever NotImplementedException, car il est uniquement appelé dans un scénario avancé impliquant l’infrastructure Reference Tracker.

// 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();
}

Pour implémenter la ComputeVtables() méthode, choisissez les types managés que vous souhaitez prendre en charge. Pour ce tutoriel, nous allons prendre en charge les deux interfaces précédemment définies (IDemoGetType et IDemoStoreType) et un type managé qui implémente les deux interfaces (DemoImpl).

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

Pour la CreateObject() méthode, vous devez également déterminer ce que vous souhaitez prendre en charge. Dans ce cas, cependant, nous connaissons uniquement les interfaces COM qui nous intéressent, et non les classes COM. Les interfaces consommées du côté COM sont les mêmes que celles que nous projetons du côté .NET (autrement dit, IDemoGetType et IDemoStoreType).

Nous n’implémenterons ReleaseObjects() pas dans ce tutoriel.

Étape 2 : Implémenter ComputeVtables()

Commençons par le Managed Object Wrapper – ce wrapper est plus simple. Vous allez créer une table de méthodes virtuelles ou une table virtuelle pour chaque interface afin de les projeter dans l’environnement COM. Pour ce tutoriel, vous allez définir une table virtuelle en tant que séquence de pointeurs, où chaque pointeur représente une implémentation d’une fonction sur une interface . L’ordre est très important ici. Dans COM, chaque interface hérite de IUnknown. Le IUnknown type a trois méthodes définies dans l’ordre suivant : QueryInterface(), AddRef()et Release(). Après les méthodes IUnknown, viennent les méthodes spécifiques de l’interface. Par exemple, considérez IDemoGetType et IDemoStoreType. Conceptuellement, les tables virtuelles des types seraient les suivantes :

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

En regardant DemoImpl, nous avons déjà une implémentation pour GetString() et StoreString(), mais qu’en est-il des fonctions IUnknown ? L’implémentation d’une IUnknown instance dépasse le cadre de ce didacticiel, mais elle peut être effectuée manuellement dans ComWrappers. Toutefois, dans ce tutoriel, vous allez laisser le runtime gérer cette partie. Vous pouvez obtenir l’implémentation IUnknown à l’aide de la ComWrappers.GetIUnknownImpl() méthode.

Il peut sembler que vous avez implémenté toutes les méthodes, mais malheureusement, seules les IUnknown fonctions sont consommables dans une table com vtable. Étant donné que COM est en dehors du runtime, vous devez créer des pointeurs de fonction natifs vers votre DemoImpl implémentation. Pour ce faire, utilisez des pointeurs de fonction C# et le UnmanagedCallersOnlyAttribute. Vous pouvez créer une fonction à insérer dans la table virtuelle en créant une static fonction qui imite la signature de la fonction COM. Voici un exemple de signature COM pour IDemoGetType.GetString() : rappel de l’ABI COM que le premier argument est l’instance elle-même.

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

L’implémentation wrapper de IDemoGetType.GetString() doit consister en une logique de marshaling, puis une répartition vers l’objet managé encapsulé. Tout l’état d'expédition est contenu dans l’argument fourni _this. L’argument _this sera en fait de type ComInterfaceDispatch*. Ce type représente une structure de bas niveau avec un seul champ, Vtablequi sera abordé ultérieurement. Des détails supplémentaires sur ce type et sa disposition sont des détails d’implémentation du runtime et ne doivent pas être pris en compte. Pour récupérer l’instance managée à partir d’une ComInterfaceDispatch* instance, utilisez le code suivant :

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

Maintenant que vous disposez d’une méthode C# qui peut être insérée dans une table virtuelle, vous pouvez construire la table virtuelle. Notez comment l'allocation de mémoire avec RuntimeHelpers.AllocateTypeAssociatedMemory() est effectuée d'une manière qui fonctionne avec des assemblies déchargeables.

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;

L’allocation de tables virtuelles est la première partie de l’implémentation ComputeVtables(). Vous devez également construire des définitions COM complètes pour les types que vous envisagez de prendre en charge : pensez DemoImpl et quelles parties de celui-ci doivent être utilisables à partir de COM. À l’aide des tables virtuelles construites, vous pouvez maintenant créer une série d’instances ComInterfaceEntry qui représentent l’affichage complet de l’objet managé dans COM.

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;

L’allocation des vtables et des entrées pour l'enveloppe d'objet géré peut et doit être effectuée à l’avance, car les données peuvent être utilisées pour toutes les instances du type. Le travail ici peut être effectué dans un static constructeur ou un initialiseur de module, mais il doit être effectué à l’avance afin que la ComputeVtables() méthode soit aussi simple et rapide que possible.

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;
}

Une fois que vous avez implémenté la ComputeVtables() méthode, la ComWrappers sous-classe peut produire des wrappers d’objets managés pour les instances de DemoImpl. N’oubliez pas que le wrapper d’objet managé retourné à partir de l’appel à GetOrCreateComInterfaceForObject() est de type IUnknown*. Si l’API d'origine qui est transmise au wrapper nécessite une autre interface, une Marshal.QueryInterface() pour cette interface doit être réalisée.

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

Étape 3 : Implémenter CreateObject()

La construction d’un wrapper d’objet natif offre plus d’options d’implémentation et beaucoup plus de nuances que la construction d’un wrapper d’objet managé. La première question à résoudre est de savoir à quel point la sous-classe sera permissive dans la prise en charge des types COM. Pour prendre en charge tous les types COM, ce qui est possible, vous devez écrire une quantité substantielle de code ou utiliser des utilisations intelligentes de Reflection.Emit. Pour ce tutoriel, vous allez uniquement prendre en charge les instances COM qui implémentent les deux IDemoGetType et IDemoStoreType. Étant donné que vous savez qu’il existe un ensemble fini et qu’une instance COM fournie doit implémenter les deux interfaces, vous pouvez fournir un wrapper défini de manière statique unique ; Toutefois, les cas dynamiques sont assez courants dans COM que nous allons explorer les deux options.

Wrapper d’objet natif statique

Examinons d’abord l’implémentation statique. Le wrapper d’objet natif statique implique la définition d’un type managé qui implémente les interfaces .NET et peut transférer les appels sur le type managé à l’instance COM. Un aperçu approximatif de l'encapsulation statique suit.

// 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();
}

Pour construire une instance de cette classe et la fournir en tant que wrapper, vous devez définir une stratégie. Si ce type est utilisé comme wrapper, il semblerait que, dans la mesure où il implémente les deux interfaces, l’instance COM sous-jacente doit également implémenter les deux interfaces. Étant donné que vous adoptez cette stratégie, vous aurez besoin de confirmer cela par le biais d’appels à l’instance COM Marshal.QueryInterface().

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 d’objet natif dynamique

Les wrappers dynamiques sont plus flexibles, car ils permettent aux types d’être interrogés au moment de l’exécution au lieu de statiquement. Pour fournir cette prise en charge, vous allez utiliser IDynamicInterfaceCastable. Notez que DemoNativeDynamicWrapper implémente uniquement cette interface. La fonctionnalité que fournit l’interface est l’occasion de déterminer quel type est pris en charge lors de l’exécution. La source de ce didacticiel effectue une vérification statique lors de la création, mais c’est simplement pour le partage de code, car la vérification peut être différée jusqu’à ce qu’un appel soit effectué .DemoNativeDynamicWrapper.IsInterfaceImplemented()

// 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();
}

Examinons l’une des interfaces que DemoNativeDynamicWrapper prendra en charge dynamiquement. Le code suivant montre l’implémentation de IDemoStoreType en utilisant la fonctionnalité méthodes d’interface par défaut.

[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);
    }
}

Il existe deux points importants à noter dans cet exemple :

  1. Attribut DynamicInterfaceCastableImplementationAttribute . Cet attribut est requis sur n’importe quel type retourné à partir d’une IDynamicInterfaceCastable méthode. Il offre l’avantage supplémentaire de faciliter le découpage du code IL, ce qui signifie que les scénarios AOT sont plus fiables.
  2. La diffusion sur DemoNativeDynamicWrapper. Cela fait partie de la nature dynamique de IDynamicInterfaceCastable. Le type qui est retourné par IDynamicInterfaceCastable.GetInterfaceImplementation() est utilisé pour « couvrir » le type qui implémente IDynamicInterfaceCastable. L'essentiel ici est que le this pointeur n’est pas ce qu’il prétend être parce que nous permettons à un cas de DemoNativeDynamicWrapper à IDemoStoreTypeNativeWrapper.

Transférer des appels à l’instance COM

Quel que soit le wrapper d’objet natif utilisé, vous avez besoin de la possibilité d’appeler des fonctions sur une instance COM. L’implémentation de IDemoStoreTypeNativeWrapper.StoreString() peut servir d’exemple d’utilisation de unmanaged pointeurs de fonction C#.

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);
    }
}

Examinons le déréférencement de l’instance COM pour accéder à son implémentation vtable. L’ABI COM définit que le premier pointeur d’un objet est vers la table virtuelle du type et, à partir de là, l’emplacement souhaité est accessible. Supposons que l’adresse de l’objet COM est 0x10000. La première valeur de taille du pointeur doit être l’adresse de la table virtuelle , dans cet exemple 0x20000. Une fois que vous êtes à la table virtuelle, vous recherchez le quatrième emplacement (index 3 dans une indexation basée sur zéro) pour accéder à l’implémentation StoreString().

COM instance
0x10000  0x20000

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

Le fait d'avoir le pointeur de fonction vous permet ensuite d'appeler cette fonction membre sur cet objet en passant l'instance de l'objet en tant que premier paramètre. Ce modèle doit être familier en fonction des définitions de fonction de l’implémentation du wrapper d’objet managé.

Une fois la CreateObject() méthode implémentée, la ComWrappers sous-classe peut produire des wrappers d’objets natifs pour les instances COM qui implémentent les deux IDemoGetType et IDemoStoreType.

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

Étape 4 : Gérer les détails de la durée de vie du wrapper d’objet natif

Les implémentations ComputeVtables() et CreateObject() ont couvert certains détails concernant la durée de vie du wrapper, mais d'autres considérations doivent être prises en compte. Bien que cela puisse être une courte étape, il peut également augmenter considérablement la complexité de la ComWrappers conception.

Contrairement au wrapper d’objet managé, qui est contrôlé par des appels à ses méthodes AddRef() et Release(), la durée de vie d’un wrapper d’objet natif est gérée de manière non déterministe par le GC. La question est la suivante : quand l’enveloppe d'objet natif appelle-t-elle Release() sur l’instance IntPtr COM ? Il y a deux grandes catégories :

  1. Le finaliseur de l'enveloppe d'objet natif est responsable de l'appel de la méthode de l'instance Release() COM. Il s’agit du seul moment où il est sûr d’appeler cette méthode. À ce stade, il a été correctement déterminé par le GC qu’il n’existe aucune autre référence au wrapper d’objet natif dans le runtime .NET. Il peut y avoir une certaine complexité ici si vous supportez correctement COM Apartments ; pour plus d’informations, consultez la section Autres considérations.

  2. Le wrapper d’objet natif implémente IDisposable et appelle Release() dans Dispose().

Remarque

Le IDisposable modèle ne doit être pris en charge que si, pendant l’appel CreateObject(), l’indicateur CreateObjectFlags.UniqueInstance a été transmis. Si cette exigence n’est pas suivie, il est possible que les wrappers d’objets natifs supprimés soient réutilisés après avoir été supprimés.

Utilisation de la ComWrappers sous-classe

Vous disposez maintenant d’une ComWrappers sous-classe qui peut être testée. Pour éviter de créer une bibliothèque native qui retourne une instance COM qui implémente IDemoGetType et IDemoStoreType, vous allez utiliser le wrapper d’objet managé et le traiter comme une instance COM . Cela doit être possible pour le transmettre de toute façon à COM.

Nous allons d’abord créer un wrapper d’objet managé. Instanciez une DemoImpl instance et affichez son état de chaîne actuel.

var demo = new DemoImpl();

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

Vous pouvez maintenant créer une instance de DemoComWrappers et un encapsuleur d'objet géré que vous pouvez ensuite passer dans un environnement COM.

var cw = new DemoComWrappers();

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

Au lieu de passer le wrapper d’objet managé à un environnement COM, faites semblant que vous venez de recevoir cette instance COM. Vous allez donc créer un wrapper d’objet natif pour celui-ci à la place.

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

Avec le wrapper d’objet natif, vous devez être en mesure de le convertir en une des interfaces souhaitées et de l’utiliser comme objet managé normal. Vous pouvez examiner l’instance DemoImpl et observer l’impact des opérations sur le wrapper d’objet natif qui encapsule un wrapper d’objet managé à son tour encapsulant l’instance managée.

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}");

Étant donné que votre ComWrapper sous-classe a été conçue pour prendre en charge CreateObjectFlags.UniqueInstance, vous pouvez nettoyer immédiatement le wrapper d’objet natif au lieu d’attendre qu’un GC se produise.

(rcw as IDisposable)?.Dispose();

Activation COM avec ComWrappers

La création d’objets COM est généralement effectuée via l’activation COM , un scénario complexe en dehors de l’étendue de ce document. Pour fournir un modèle conceptuel à suivre, nous présentons l’API CoCreateInstance() , utilisée pour l’activation COM et illustrant comment elle peut être utilisée avec ComWrappers.

Supposons que vous disposez du code C# suivant dans votre application. L’exemple ci-dessous utilise CoCreateInstance() pour activer une classe COM et le système COM interop intégré pour marshaler l’instance COM sur l’interface appropriée. Notez que l'utilisation de typeof(I).GUID est limitée à une assertion et est un cas d'utilisation de la réflexion qui peut avoir un impact sur la compatibilité avec AOT.

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

La conversion de l'utilisation ci-dessus pour ComWrappers implique d'enlever MarshalAs(UnmanagedType.Interface) du CoCreateInstance() P/Invoke et d'exécuter le marshalling manuellement.

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

Il est également possible d’extraire les fonctions de style usine comme ActivateClass<I> en incluant la logique d’activation dans le constructeur de classe pour une enveloppe d’objet natif. Le constructeur peut utiliser l’API ComWrappers.GetOrRegisterObjectForComInstance() pour associer l’objet managé nouvellement construit à l’instance COM activée.

Considérations supplémentaires

La compilation native AOT : la compilation anticipée (AOT) offre un coût de démarrage amélioré, car la compilation JIT est évitée. La suppression de la nécessité de la compilation JIT est également souvent nécessaire sur certaines plateformes. La prise en charge d’AOT était un objectif de l’API ComWrappers, mais toute implémentation de wrapper doit veiller à ne pas introduire par inadvertance des cas où AOT tombe en panne, tels que l'utilisation de la réflexion. La Type.GUID propriété est un exemple d’utilisation de la réflexion, mais de manière non évidente. La Type.GUID propriété utilise la réflexion pour inspecter les attributs du type, puis potentiellement le nom du type et l’assembly contenant, afin de générer sa valeur.

Génération de la source : la plupart du code nécessaire pour l’interopérabilité COM et une ComWrappers implémentation peuvent probablement être générés automatiquement par certains outils. La source pour les deux types de wrappers peut être générée en fonction des définitions COM appropriées, par exemple, bibliothèque de types (TLB), IDL ou assembly d'interopérabilité primaire (PIA).

Inscription globale : étant donné que l’API ComWrappers a été conçue comme une nouvelle phase d’interopérabilité COM, il fallait avoir un moyen d’intégrer partiellement le système existant. Il existe des méthodes statiques qui ont un impact global sur l’API ComWrappers qui autorisent l’inscription d’une instance globale pour diverses prise en charge. Ces méthodes sont conçues pour ComWrappers les instances qui s’attendent à fournir une prise en charge COM Interop complète dans tous les cas, semblable au système COM Interop incorporé.

Prise en charge de Reference Tracker : cette prise en charge est principale utilisée pour les scénarios WinRT et représente un scénario avancé. Pour la plupart des implémentations ComWrapper, soit un indicateur CreateComInterfaceFlags.TrackerSupport soit un CreateObjectFlags.TrackerObject doit lever un NotSupportedException. Si vous souhaitez activer cette prise en charge, peut-être sur une plateforme Windows ou même non-Windows, il est vivement recommandé de référencer la chaîne d’outils C#/WinRT.

Outre la durée de vie, le système de type et les fonctionnalités fonctionnelles décrites précédemment, une implémentation de ComWrappers compatible COM nécessite des considérations supplémentaires. Pour toute implémentation qui sera utilisée sur la plateforme Windows, voici les considérations suivantes :

  • Appartements – La structure organisationnelle de COM pour le threading est appelée « Appartements » et a des règles strictes qui doivent être suivies pour les opérations stables. Ce tutoriel n’implémente pas les wrappers d’objets natifs prenant en charge l’appartement, mais toute implémentation prête pour la production doit être compatible avec l’appartement. Pour ce faire, nous vous recommandons d’utiliser l’API RoGetAgileReference introduite dans Windows 8. Pour les versions antérieures à Windows 8, tenez compte de la table d’interface globale.

  • Sécurité : COM fournit un modèle de sécurité complet pour l’activation de classe et l’autorisation proxiée.