Cualificación de tipos de .NET para la interoperación con COM

Exposición de tipos de .NET a COM

Si tiene previsto exponer tipos en un ensamblado a las aplicaciones COM, tenga en cuenta los requisitos de interoperabilidad COM en tiempo de diseño. Los tipos administrados (clase, interfaz, estructura y enumeración) se integran fácilmente con los tipos COM si se cumplen las directrices siguientes:

  • Las clases deben implementar interfaces de forma explícita.

    Aunque la interoperabilidad COM proporciona un mecanismo para generar automáticamente una interfaz que contiene todos los miembros de la clase y los miembros de su clase base, es mucho mejor proporcionar interfaces explícitas. La interfaz generada automáticamente se denomina interfaz de clase. Para obtener instrucciones, consulte Presentar la interfaz de clase.

    Puede usar Visual Basic, C# y C++ para incorporar las definiciones de interfaz en el código, en lugar de tener que usar el Lenguaje de definición de interfaz (IDL) o su equivalente. Para obtener información detallada de la sintaxis, vea la documentación del lenguaje.

  • Los tipos administrados deben ser públicos.

    Solo los tipos públicos de un ensamblado se registran y se exportan a la biblioteca de tipos. Como resultado, solo los tipos públicos son visibles para COM.

    Los tipos administrados exponen características a otro código administrado que es posible que no se expongan a COM. Por ejemplo, los constructores con parámetros, los métodos estáticos y los campos de constante no se exponen a los clientes COM. Además, como el tiempo de ejecución serializa los datos dentro y fuera de un tipo, es posible que los datos se copien o se transformen.

  • Los métodos, las propiedades, los campos y los eventos deben ser públicos.

    Los miembros de tipos públicos también deben ser públicos si van a ser visibles para COM. La aplicación de ComVisibleAttribute permite restringir la visibilidad de un ensamblado, un tipo público o miembros públicos de un tipo público. De forma predeterminada, todos los tipos y miembros públicos son visibles.

  • Los tipos deben tener un constructor público sin parámetros para que se activen desde COM.

    Los tipos públicos administrados son visibles para COM. Pero sin un constructor público sin parámetros (un constructor sin argumentos), los clientes COM no pueden crear el tipo. Los clientes COM todavía pueden usar el tipo si se activa por otros medios.

  • Los tipos no pueden ser abstractos.

    Ni los clientes COM ni los clientes .NET pueden crear tipos abstractos.

Cuando se exporta a COM, se simplifica la jerarquía de herencia de un tipo administrado. El control de versiones también difiere entre los entornos administrados y no administrados. Los tipos expuestos a COM no tienen las mismas características de control de versiones que otros tipos administrados.

Consumo de tipos COM de .NET

Si tiene previsto consumir tipos COM de .NET y no desea usar herramientas como Tlbimp.exe (importador de bibliotecas de tipos), debe seguir estas instrucciones:

  • Las interfaces deben tener aplicado ComImportAttribute.
  • Las interfaces deben tener aplicado GuidAttribute con el identificador de la interfaz COM.
  • Las interfaces deben tener aplicado InterfaceTypeAttribute para especificar el tipo de interfaz base de esta interfaz (IUnknown, IDispatch o IInspectable).
    • La opción predeterminada es tener el tipo base de IDispatch y anexar los métodos declarados a la tabla de funciones virtuales esperada para la interfaz.
    • Solo .NET Framework admite la especificación de un tipo base de IInspectable.

Estas instrucciones proporcionan los requisitos mínimos para escenarios comunes. Existen muchas más opciones de personalización, que se describen en Aplicación de atributos de interoperabilidad.

Definición de interfaces COM en .NET

Cuando el código .NET intenta llamar a un método en un objeto COM mediante una interfaz con el atributo ComImportAttribute, debe crear una tabla de funciones virtuales (también conocida como vtable o vftable) para formar la definición de .NET de la interfaz a fin de determinar el código nativo al que se debe llamar. Este proceso es complejo. En los ejemplos siguientes se muestran algunos casos sencillos.

Considere una interfaz COM con algunos métodos:

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

Para esta interfaz, en la tabla siguiente se describe su diseño de tabla de funciones virtuales:

Ranura de tabla de funciones virtuales IComInterface Nombre del método
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2

Cada método se agrega a la tabla de funciones virtuales en el orden en que se declaró. El compilador de C++ define el orden determinado, pero en casos simples sin sobrecargas, el orden de declaración define el orden de la tabla.

Declare una interfaz .NET que corresponda a esta interfaz de la siguiente manera:

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

InterfaceTypeAttribute especifica la interfaz base. Proporciona algunas opciones:

Valor de ComInterfaceType Tipo de interfaz base Comportamiento de los miembros en la interfaz con atributos
InterfaceIsIUnknown IUnknown La tabla de funciones virtuales tiene primero los miembros de IUnknown y, luego, los miembros de esta interfaz en orden de declaración.
InterfaceIsIDispatch IDispatch Los miembros no se agregan a la tabla de funciones virtuales. Solo son accesibles mediante IDispatch.
InterfaceIsDual IDispatch La tabla de funciones virtuales tiene primero los miembros de IDispatch y, luego, los miembros de esta interfaz en orden de declaración.
InterfaceIsIInspectable IInspectable La tabla de funciones virtuales tiene primero los miembros de IInspectable y, luego, los miembros de esta interfaz en orden de declaración. Solo se admite en .NET Framework.

Herencia de interfaz COM y .NET

El sistema de interoperabilidad COM que usa ComImportAttribute no interactúa con la herencia de interfaz, por lo que puede provocar un comportamiento inesperado a menos que se realicen algunos pasos de mitigación.

El generador de código fuente COM que usa el atributo System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute sí interactúa con la herencia de interfaz, por lo que se comporta más según lo esperado.

Herencia de interfaz COM en C++

En C++, los desarrolladores pueden declarar interfaces COM que derivan de otras interfaces COM de la siguiente manera:

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

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

Este estilo de declaración se usa normalmente como mecanismo para agregar métodos a objetos COM sin cambiar las interfaces existentes, lo que sería un cambio importante. Este mecanismo de herencia da como resultado los siguientes diseños de tabla de funciones virtuales:

Ranura de tabla de funciones virtuales IComInterface Nombre del método
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
Ranura de tabla de funciones virtuales IComInterface2 Nombre del método
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

Como resultado, es fácil llamar a un método definido en IComInterface desde un objeto IComInterface2*. En concreto, llamar a un método en una interfaz base no requiere una llamada a QueryInterface para obtener un puntero a la interfaz base. Además, C++ permite una conversión implícita de IComInterface2* a IComInterface*, que está bien definida y le permite evitar llamar de nuevo a QueryInterface. Como resultado, en C o C++, nunca tiene que llamar a QueryInterface para llegar al tipo base si no quiere hacerlo, lo que puede permitir algunas mejoras de rendimiento.

Nota

Las interfaces de WinRT no siguen este modelo de herencia. Se definen para seguir el mismo modelo que el modelo de interoperabilidad COM basado en [ComImport] de .NET.

Herencia de interfaz con ComImportAttribute

En .NET, el código de C# que se parece a la herencia de interfaz no es realmente la herencia de interfaz. Observe el código siguiente:

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

Este código no dice "J implementa I". El código dice en realidad, "cualquier tipo que implemente J también debe implementar I". Esta diferencia conduce a la decisión fundamental de diseño que hace que la herencia de la interfaz en interoperabilidad basada en la interoperabilidad basada en ComImportAttribute no sea ergonómica. Las interfaces siempre se consideran por sí mismas; la lista de interfaces base de una interfaz no tiene ningún efecto en los cálculos para determinar una tabla de funciones virtuales para una interfaz de .NET determinada.

Como resultado, el equivalente natural del ejemplo anterior de la interfaz COM de C++ conduce a un diseño de tabla de funciones virtuales diferente.

Código de C#:

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

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

Diseños de tablas de funciones virtuales:

Ranura de tabla de funciones virtuales IComInterface Nombre del método
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
Ranura de tabla de funciones virtuales IComInterface2 Nombre del método
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface2::Method3

Dado que estas tablas de funciones virtuales difieren del ejemplo de C++, se producirán problemas graves en tiempo de ejecución. La definición correcta de estas interfaces en .NET con ComImportAttribute es la siguiente:

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

En el nivel de metadatos, IComInterface2 no implementa IComInterface, sino que solo especifica que los implementadores de IComInterface2 también deben implementar IComInterface. Por lo tanto, cada método de los tipos de interfaz base debe volver a declararse.

Herencia de interfaz con GeneratedComInterfaceAttribute (.NET 8 y versiones posteriores)

El generador de código fuente COM desencadenado por GeneratedComInterfaceAttribute implementa la herencia de interfaz de C# como herencia de interfaz COM, por lo que las tablas de funciones virtuales se disponen según lo previsto. Si toma el ejemplo anterior, la definición correcta de estas interfaces en .NET con System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute es la siguiente:

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

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

No es necesario volver a declarar los métodos de las interfaces base y no se debe hacer. En la tabla siguiente se describen las tablas de funciones virtuales resultantes:

Ranura de tabla de funciones virtuales IComInterface Nombre del método
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
Ranura de tabla de funciones virtuales IComInterface2 Nombre del método
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

Como puede ver, estas tablas coinciden con el ejemplo de C++, por lo que estas interfaces funcionarán correctamente.

Vea también