Partager via


Tutoriel : Utiliser l’API ComWrappers

Dans ce tutoriel, vous allez apprendre à sous-classer correctement le type ComWrappers pour fournir une solution COM Interop optimisée et compatible avec une anticipation (AOT). Avant de commencer ce tutoriel, vous devez vous familiariser 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 vont illustrer :

  • Le marshaling et le démarshaling des types au-delà de la limite COM/.NET.
  • Deux approches distinctes de la consommation d’objets COM natifs dans .NET.
  • Un modèle recommandé pour activer un service COM Interop personnalisé dans .NET 5 et version supérieure.

Tout le code source utilisé dans ce tutoriel est disponible dans le dépôt dotnet/samples.

Remarque

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

Définitions C#

interface IDemoGetType
{
    string? GetString();
}

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

Définitions C++ Win32

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 conception de ComWrappers

L’API ComWrappers a été conçue pour fournir l’interaction minimale nécessaire pour accomplir COM Interop avec le runtime .NET 5+. Cela signifie que la plupart des subtilités qui existent avec le système COM Interop intégré ne sont pas présentes et doivent être créées à partir de blocs de construction de base. Les deux principales responsabilités de l’API sont les suivantes :

  • Identification efficace des objets (par exemple, mappage entre une instance IUnknown* et un objet managé).
  • Interaction du récupérateur de mémoire (GC).

Cette efficacité est obtenue en faisant passer la création et l’acquisition des wrappers nécessaires par l’API ComWrappers.

Étant donné que l’API ComWrappers a si peu de responsabilités, il va de soi que la plupart du travail d’interopérabilité doit être géré par le consommateur. C’est indéniable. Toutefois, le travail supplémentaire est en grande partie mécanique et peut être effectué par une solution de génération de source. Par exemple, la chaîne d’outils C#/WinRT est une solution de génération de source qui s’appuie sur ComWrappers pour fournir la prise en charge de l’interopérabilité WinRT.

Implémenter une sous-classe ComWrappers

Fournir une sous-classe ComWrappers implique de 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 les grandes lignes de la sous-classe, nous devons définir quelques termes.

Wrapper d’objet managé : Les objets .NET managés nécessitent des wrappers pour permettre une utilisation à partir d’un environnement non-.NET. Ces wrappers sont historiquement appelés wrappers CCW (COM Callable Wrappers).

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

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

Pour étendre le type ComWrappers, 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 managé et un wrapper d’objet natif. La méthode ReleaseObjects() est utilisée par le runtime pour demander 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 méthode ReleaseObjects() peut simplement lever NotImplementedException, car il est appelé uniquement dans un scénario avancé impliquant le framework de suivi de référence.

// 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 méthode ComputeVtables(), déterminez 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 méthode CreateObject(), vous avez également besoin de déterminer ce que vous souhaitez prendre en charge. Dans ce cas, néanmoins, nous connaissons uniquement les interfaces COM qui nous intéressent, pas les classes COM. Les interfaces consommées côté COM sont les mêmes que celles que nous projetons depuis le côté .NET (c’est-à-dire IDemoGetType et IDemoStoreType).

Nous n’allons pas implémenter ReleaseObjects() dans le cadre de ce tutoriel.

Étape 2 : Implémenter ComputeVtables()

Commençons par le wrapper d’objet managé : ces wrappers sont plus faciles. Vous allez créer une table de méthode virtuelle, ou vtable, pour chaque interface afin de les projeter dans l’environnement COM. Pour ce tutoriel, vous allez définir une vtable 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 type IUnknown a trois méthodes définies dans l’ordre suivant : QueryInterface(), AddRef() et Release(). Après les méthodes IUnknown viennent les méthodes d’interface spécifiques. Par exemple, considérez IDemoGetType et IDemoStoreType. D’un point de vue conceptuel, les vtables pour les types ressemblent à ceci :

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

En examinant DemoImpl, nous avons déjà une implémentation pour GetString() et StoreString(), mais qu’en est-il des fonctions IUnknown ? La manière d’implémenter une instance IUnknown dépasse le cadre de ce tutoriel, mais vous pouvez le faire 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 méthode ComWrappers.GetIUnknownImpl().

Les apparences peuvent laisser croire que vous avez implémenté toutes les méthodes, mais malheureusement, seules les fonctions IUnknown sont consommables dans une vtable COM. Étant donné que COM est extérieur au runtime, vous allez avoir besoin de créer des pointeurs de fonction natifs vers votre implémentation DemoImpl. Pour cela, vous utilisez des pointeurs de fonction C# et UnmanagedCallersOnlyAttribute. Vous pouvez créer une fonction à insérer dans la vtable en créant une fonction static qui imite la signature de la fonction COM. Voici un exemple de signature COM pour IDemoGetType.GetString() : n’oubliez pas qu’à partir de l’ABI COM, le premier argument correspond à l’instance elle-même.

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

L’implémentation du wrapper de IDemoGetType.GetString() doit se composer d’une logique de marshaling, puis d’un dispatch vers l’objet managé qui est wrappé. Tout l’état du dispatch est contenu dans l’argument _this fourni. L’argument _this sera en fait de type ComInterfaceDispatch*. Ce type représente une structure basique avec un seul champ, Vtable, que nous allons décrire plus tard. D’autres détails de ce type et de sa disposition constituent un détail d’implémentation du runtime et ne doivent pas en dépendre. Pour récupérer l’instance managée à partir d’une instance ComInterfaceDispatch*, 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 vtable, vous pouvez construire la vtable. Notez l’utilisation de RuntimeHelpers.AllocateTypeAssociatedMemory() pour allouer de la mémoire d’une manière qui fonctionne avec des assemblys 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 vtables correspond à la première partie de l’implémentation de ComputeVtables(). Vous devez également construire des définitions COM complètes pour les types que vous envisagez de prendre en charge. Pensez à DemoImpl et déterminez lesquels de ses composants doivent être utilisables à partir de COM. À l’aide des vtables construites, vous pouvez maintenant créer une série d’instances ComInterfaceEntry qui représente la vue complète 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 de vtables et d’entrées pour le wrapper d’objet managé 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 constructeur static ou un initialiseur de module, mais il doit être effectué à l’avance afin que la méthode ComputeVtables() 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 méthode ComputeVtables(), la sous-classe ComWrappers 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 native passée au wrapper nécessite une autre interface, un Marshal.QueryInterface() doit être effectué pour cette interface.

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 comporte plus d’options d’implémentation et beaucoup plus de nuances que la construction d’un wrapper d’objet managé. La première question à se poser porte sur la permissivité de la sous-classe ComWrappers dans la prise en charge des types COM. Pour prendre en charge tous les types COM, ce qui est possible, vous allez avoir besoin d’écrire une quantité importante de code ou de recourir à des utilisations intelligentes de Reflection.Emit. Pendant ce tutoriel, vous allez uniquement prendre en charge des instances COM qui implémentent à la fois IDemoGetType et IDemoStoreType. Étant donné que vous savez qu’il existe un ensemble fini et une restriction au fait que toute instance COM fournie doit implémenter les deux interfaces, vous pouvez fournir un wrapper unique défini de manière statique. Toutefois, les cas dynamiques sont suffisamment courants dans COM pour que nous explorions les deux options.

Wrapper d’objet natif statique

Examinons d’abord l’implémentation statique. Le wrapper d’objet natif statique implique de définir un type managé qui implémente les interfaces .NET et peut transmettre les appels sur le type managé à l’instance COM. Voici un schéma approximatif du wrapper statique.

// 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é en tant que wrapper, il semble que, étant donné qu’il implémente les deux interfaces, l’instance COM sous-jacente doive également implémenter les deux interfaces. Étant donné que vous adoptez cette stratégie, vous allez avoir besoin de le confirmer par des appels à Marshal.QueryInterface() sur l’instance COM.

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 plutôt que de manière statique. Pour assurer cette prise en charge, vous allez utiliser IDynamicInterfaceCastable. Vous trouverez plus d’informations ici. Observez que DemoNativeDynamicWrapper implémente uniquement cette interface. La fonctionnalité que fournit l’interface donne l’occasion de déterminer quel type est pris en charge au moment de l’exécution. La source utilisée pour ce tutoriel effectue une vérification statique lors de la création, mais simplement pour le partage de code, car la vérification peut être différée jusqu’à ce qu’un appel à DemoNativeDynamicWrapper.IsInterfaceImplemented() soit effectué.

// 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 va prendre en charge dynamiquement. Le code suivant fournit l’implémentation de IDemoStoreType à l’aide de la fonctionnalité des 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);
    }
}

Deux points importants sont à noter dans cet exemple :

  1. Attribut DynamicInterfaceCastableImplementationAttribute. Cet attribut est obligatoire sur tout type retourné à partir d’une méthode IDynamicInterfaceCastable. Il offre l’avantage supplémentaire de faciliter le découpage IL, ce qui signifie que les scénarios AOT sont plus fiables.
  2. Le cast en DemoNativeDynamicWrapper. Celui-ci fait partie de la nature dynamique de IDynamicInterfaceCastable. Le type retourné à partir de IDynamicInterfaceCastable.GetInterfaceImplementation() est utilisé pour « recouvrir » le type qui implémente IDynamicInterfaceCastable. L’idée générale ici est que le pointeur this n’est pas ce qu’il prétend être, car nous autorisons un cast de DemoNativeDynamicWrapper en IDemoStoreTypeNativeWrapper.

Transmettre des appels à l’instance COM

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

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 à l’implémentation de sa vtable. L’ABI COM définit le premier pointeur d’un objet vers la vtable 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 dimensionnée par un pointeur doit être l’adresse de la vtable (dans cet exemple, 0x20000). Une fois que vous accédez à la vtable, vous recherchez le quatrième emplacement (index 3 dans l’indexation de base zéro) pour accéder à l’implémentation de 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 de dispatcher vers cette fonction membre sur cet objet en passant l’instance de l’objet en tant que premier paramètre. Ce modèle doit sembler familier au regard des définitions de fonction de l’implémentation du wrapper d’objet managé.

Une fois la méthode CreateObject() implémentée, la sous-classe ComWrappers peut produire des wrappers d’objets natifs pour des instances COM qui implémentent à la fois 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 de la durée de vie du wrapper, mais d’autres éléments sont à considérer. Bien qu’il puisse s’agir d’une étape courte, elle peut également augmenter considérablement la complexité de la conception des ComWrappers.

Contrairement au wrapper d’objet managé, qui est contrôlé par les 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 récupérateur de mémoire (GC). La question est la suivante : quand le wrapper d’objet natif appelle-t-il Release() sur le IntPtr qui représente l’instance COM ? Il existe deux compartiments généraux :

  1. Le finaliseur du wrapper d’objet natif est chargé d’appeler la méthode Release() de l’instance COM. C’est le seul moment sans risque d’appeler cette méthode. À ce stade, le GC a déterminé correctement qu’il n’existe aucune autre référence au wrapper d’objet natif dans le runtime .NET. Cela peut s’avérer complexe ici si vous prenez correctement en charge des cloisonnements COM. Pour plus d’informations, consultez la section Considérations supplémentaires.

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

Notes

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

Utilisation de la sous-classe ComWrappers

Vous disposez maintenant d’une sous-classe ComWrappers 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 en tant qu’instance COM. Cela doit être possible pour pouvoir le transmettre à COM de toute façon.

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

var demo = new DemoImpl();

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

Vous pouvez maintenant créer un instance de DemoComWrappers et un wrapper d’objet managé 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 comme si vous veniez de recevoir cette instance COM, afin de plutôt créer un wrapper d’objet natif pour lui.

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

Avec le wrapper d’objet natif, vous devez être en mesure de le caster en l’une des interfaces souhaitées et de l’utiliser en tant qu’objet managé normal. Vous pouvez examiner l’instance DemoImpl et observer l’impact des opérations sur le wrapper d’objet natif qui enveloppe un wrapper d’objet managé qui, à son tour, enveloppe 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 sous-classe ComWrapper 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 s’effectue généralement par le biais d’une activation COM, un scénario complexe qui sort du cadre de ce document. Afin de fournir un modèle conceptuel à suivre, nous allons présenter l’API CoCreateInstance(), utilisée pour l’activation COM, et expliquer comment elle peut être utilisée avec ComWrappers.

Supposons que vous avez le 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 vers l’interface appropriée. Notez que l’utilisation de typeof(I).GUID est limitée à une assertion et qu’il s’agit d’un cas d’utilisation d’une réflexion susceptible d’avoir un impact si le code est compatible avec une anticipation (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’exemple ci-dessus pour utiliser ComWrappers implique de supprimer MarshalAs(UnmanagedType.Interface) du P/Invoke CoCreateInstance() et d’effectuer le marshaling 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’abstraire des fonctions de style fabrique, comme ActivateClass<I>, en incluant la logique d’activation dans le constructeur de classe pour un wrapper 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

AOT native : Une compilation anticipée (AOT, Ahead-of-time) offre un coût de démarrage amélioré puisque la compilation JIT est évitée. La suppression du besoin de compilation JIT est par ailleurs 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 s’arrête, notamment lors de l’utilisation de la réflexion. La propriété Type.GUID est un exemple d’utilisation de la réflexion, mais de manière non évidente. La propriété Type.GUID utilise une 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 majeure partie du code nécessaire à COM Interop et à une implémentation de ComWrappers peut probablement être générée automatiquement par certains outils. La source des deux types de wrappers peut être générée selon les définitions COM appropriées, par exemple, la bibliothèque de types (TLB), IDL ou un assembly PIA (Primary Interop Assembly).

Inscription globale : Étant donné que l’API ComWrappers a été conçue comme une nouvelle phase de COM Interop, elle avait besoin de disposer d’un moyen de s’intégrer partiellement au système existant. Il existe des méthodes statiques à impact global sur l’API ComWrappers qui permettent d’inscrire une instance global pour diverses prises en charge. Ces méthodes sont conçues pour les instances ComWrappers supposées fournir une prise en charge de COM Interop complète dans tous les cas, à l’image du système COM Interop intégré.

Prise en charge du suivi de référence : Cette prise en charge est principalement utilisée pour les scénarios WinRT et représente un scénario avancé. Pour la plupart des implémentations de ComWrapper, un indicateur CreateComInterfaceFlags.TrackerSupport ou CreateObjectFlags.TrackerObject doit lever une NotSupportedException. Si vous souhaitez activer cette prise en charge, peut-être sur une plateforme Windows voire 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 décrites précédemment, une implémentation compatible avec COM de ComWrappers nécessite des considérations supplémentaires. Pour toute implémentation prévue pour une utilisation sur la plateforme Windows, les points suivants doivent être considérés :

  • Cloisonnements : La structure organisationnelle de COM pour le threading est appelée « cloisonnements ». Elle comporte des règles strictes qui doivent être suivies pour la stabilité des opérations. Ce tutoriel n’implémente pas des wrappers d’objets natifs prenant en charge les cloisonnements, mais toute implémentation prête pour la production doit être capable de les prendre en charge. Pour cela, nous vous recommandons d’utiliser l’API RoGetAgileReference introduite dans Windows 8. Pour les versions antérieures à Windows 8, envisagez d’utiliser la table d’interface globale.

  • Sécurité : COM fournit un modèle de sécurité riche pour l’activation des classes et les autorisations par proxy.