Qualification des types .NET en vue de l’interopérabilité COM

Exposer des types .NET à COM

Si vous envisagez d’exposer les types d’un assembly à des applications COM, prenez en compte les exigences COM Interop au moment de la conception. Les types managés (classe, interface, structure et énumération) s’intègrent parfaitement aux types COM lorsque vous respectez les consignes suivantes :

  • Les classes doivent implémenter les interfaces de manière explicite.

    Même si COM Interop fournit un mécanisme permettant de générer automatiquement une interface contenant tous les membres de la classe et de sa classe de base, il est fortement recommandé de fournir des interfaces explicites. L’interface générée automatiquement est appelée « interface de classe ». Pour obtenir des instructions, consultez Présentation de l’interface de classe.

    Vous pouvez utiliser Visual Basic, C# et C++ pour incorporer des définitions d’interface dans votre code, au lieu du langage IDL (ou équivalent). Pour plus d’informations sur la syntaxe, consultez la documentation relative à votre langage.

  • Les types managés doivent être publics.

    Seuls les types publics d’un assembly sont inscrits et exportés vers la bibliothèque de types. Par conséquent, seuls les types publics sont visibles par COM.

    Les types managés exposent des fonctionnalités à un autre code managé qui peut ne pas être exposé à COM. Par exemple, les constructeurs paramétrables, les méthodes statiques et les champs constants ne sont pas exposés aux clients COM. De plus, comme le runtime marshale les données d’un type à un autre, les données peuvent être copiées ou transformées.

  • Les méthodes, les propriétés, les champs et les événements doivent être publics.

    Les membres des types publics doivent également être publics pour être visibles par COM. Vous pouvez restreindre la visibilité d’un assembly, d’un type public ou de membres publics d’un type public en appliquant ComVisibleAttribute. Par défaut, tous les membres et types publics sont visibles.

  • Les types doivent avoir un constructeur sans paramètre public pour être activés dans COM.

    Les types publics managés sont visibles par COM. Toutefois, sans constructeur public sans paramètre (constructeur sans arguments), les clients COM ne peuvent pas créer le type. Les clients COM peuvent toujours utiliser le type s’il est activé par d’autres moyens.

  • Les types ne peuvent pas être abstraits.

    Ni les clients COM ni les clients .NET ne peuvent créer de types abstraits.

Lors de l’exportation vers COM, la hiérarchie d’héritage d’un type managé est aplatie. Le contrôle de version est également différent dans les environnements managés et non managés. Les types exposés à COM n’ont pas les mêmes caractéristiques de contrôle de version que les autres types managés.

Consommer des types COM à partir de .NET

Si vous envisagez de consommer des types COM à partir de .NET et que vous ne souhaitez pas utiliser d’outils tels que Tlbimp.exe (importateur de bibliothèques de types), vous devez suivre ces instructions :

  • Le ComImportAttribute doit être appliqué pour les interfaces.
  • Le GuidAttribute doit être appliqué pour les interfaces, avec l’ID d’interface de l’interface COM.
  • Le InterfaceTypeAttribute doit être appliqué aux interfaces pour spécifier le type d’interface de base de cette interface (IUnknown, IDispatch ou IInspectable).
    • L’option par défaut consiste à avoir le type de base de IDispatch et à ajouter les méthodes déclarées à la table de fonctions virtuelles attendue pour l’interface.
    • Seul .NET Framework prend en charge la spécification d’un type de base de IInspectable.

Ces instructions fournissent les conditions minimales requises pour les scénarios courants. De nombreuses autres options de personnalisation existent et sont décrites dans Application d’attributs d’interopérabilité.

Définir des interfaces COM dans .NET

Lorsque le code .NET tente d’appeler une méthode sur un objet COM via une interface avec l’attribut ComImportAttribute, il doit créer une table de fonctions virtuelles (également appelée vtable ou vftable) pour former la définition .NET de l’interface pour déterminer le code natif à appeler. Ce processus est complexe. Les exemples suivants montrent quelques cas simples.

Prenons l’exemple d’une interface COM avec quelques méthodes :

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

Pour cette interface, le tableau suivant décrit sa disposition de tableau de fonctions virtuelles :

Emplacement de tableau de fonctions virtuelles IComInterface Nom de la méthode
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2

Chaque méthode est ajoutée au tableau de fonctions virtuelles dans l’ordre dans lequel elle a été déclarée. L’ordre particulier est défini par le compilateur C++, mais pour des cas simples sans surcharges, l’ordre de déclaration définit l’ordre dans le tableau.

Déclarez une interface .NET qui correspond à cette interface comme suit :

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

Le InterfaceTypeAttribute spécifie l’interface de base. Il fournit quelques options :

Valeur ComInterfaceType Type d’interface de base Comportement des membres sur l’interface attribuée
InterfaceIsIUnknown IUnknown Le tableau de fonctions virtuelles possède d’abord les membres de IUnknown, puis les membres de cette interface dans l’ordre de déclaration.
InterfaceIsIDispatch IDispatch Les membres ne sont pas ajoutés au tableau de fonctions virtuelles. Ils ne sont accessibles qu’à l’aide de IDispatch.
InterfaceIsDual IDispatch Le tableau de fonctions virtuelles possède d’abord les membres de IDispatch, puis les membres de cette interface dans l’ordre de déclaration.
InterfaceIsIInspectable IInspectable Le tableau de fonctions virtuelles possède d’abord les membres de IInspectable, puis les membres de cette interface dans l’ordre de déclaration. Prise en charge uniquement sur .NET Framework.

Héritage d’interface COM et .NET

Le système COM Interop qui utilise le ComImportAttribute n’interagit pas avec l’héritage de l’interface, de sorte qu’il peut provoquer un comportement inattendu, sauf si certaines mesures d’atténuation sont prises.

Le générateur de source COM qui utilise l’attribut System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute interagit avec l’héritage de l’interface, de sorte qu’il se comporte davantage comme prévu.

Héritage d’interface COM en C++

En C++, les développeurs peuvent déclarer des interfaces COM qui dérivent d’autres interfaces COM comme suit :

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

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

Ce style de déclaration est régulièrement utilisé comme mécanisme pour ajouter des méthodes à des objets COM sans modifier les interfaces existantes, ce qui serait un changement cassant. Ce mécanisme d’héritage entraîne les dispositions du tableau de fonctions virtuelles suivantes :

Emplacement de tableau de fonctions virtuelles IComInterface Nom de la méthode
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
Emplacement de tableau de fonctions virtuelles IComInterface2 Nom de la méthode
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

Par conséquent, il est facile d’appeler une méthode définie sur IComInterface partir d’une IComInterface2*. Plus précisément, l’appel d’une méthode sur une interface de base ne nécessite pas d’appel à QueryInterface pour obtenir un pointeur vers l’interface de base. En outre, C++ permet une conversion implicite de IComInterface2* en IComInterface*, qui est bien définie et vous permet d’éviter d’appeler une nouvelle fois une QueryInterface. Par conséquent, en C ou C++, vous n’avez jamais besoin d’appeler QueryInterface pour accéder au type de base si vous ne le souhaitez pas, ce qui peut permettre d’améliorer les performances.

Notes

Les interfaces WinRT ne suivent pas ce modèle d’héritage. Elles sont définies pour suivre le même modèle que le modèle COM Interop basé sur [ComImport] dans .NET.

Héritage d’interface avec ComImportAttribute

Dans .NET, le code C# qui ressemble à l’héritage d’interface n’est pas réellement l’héritage de l’interface. Prenez le code suivant :

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

Ce code ne dit pas « J implémente I. » Le code indique en fait « tout type qui implémente J doit également implémenter I. » Cette différence conduit à la décision de conception fondamentale qui rend non ergonomique l’héritage d’interface dans l’interopérabilité basée sur ComImportAttribute. Les interfaces sont toujours considérées comme elles-mêmes. La liste d’interfaces de base d’une interface n’a aucun impact sur les calculs pour déterminer un tableau de fonctions virtuelles pour une interface .NET donnée.

Par conséquent, l’équivalent naturel de l’exemple d’interface COM C++ précédent conduit à une autre disposition de tableau de fonctions virtuelles.

Code C# :

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

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

Dispositions du tableau de fonctions virtuelles :

Emplacement de tableau de fonctions virtuelles IComInterface Nom de la méthode
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
Emplacement de tableau de fonctions virtuelles IComInterface2 Nom de la méthode
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface2::Method3

Étant donné que ces tableaux de fonctions virtuelles diffèrent de l’exemple C++, cela entraîne de graves problèmes au moment de l’exécution. La définition correcte de ces interfaces dans .NET avec ComImportAttribute est la suivante :

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

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

Au niveau des métadonnées, IComInterface2 n’implémente pas IComInterface, mais spécifie uniquement que les implémenteurs de IComInterface2 doivent également implémenter IComInterface. Ainsi, chaque méthode des types d’interface de base doit être redeclarée.

Héritage d’interface avec GeneratedComInterfaceAttribute (.NET 8 et ultérieur)

Le générateur de source COM déclenché par le GeneratedComInterfaceAttribute implémente l’héritage de l’interface C# en tant qu’héritage d’interface COM, de sorte que les tableaux de fonctions virtuelles sont disposés comme prévu. Si vous prenez l’exemple précédent, la définition correcte de ces interfaces dans .NET avec System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute est la suivante :

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

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

Les méthodes des interfaces de base n’ont pas besoin d’être redéclarées et ne doivent pas être redéclarées. Le tableau suivant décrit les tableaux de fonctions virtuelles résultants :

Emplacement de tableau de fonctions virtuelles IComInterface Nom de la méthode
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
Emplacement de tableau de fonctions virtuelles IComInterface2 Nom de la méthode
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

Comme vous pouvez le voir, ces tableaux correspondent à l’exemple C++, de sorte que ces interfaces fonctionnent correctement.

Voir aussi