Qualificar tipos .NET para interoperação COM

Expor tipos .NET ao COM

Se você pretende expor os tipos em um assembly para aplicativos COM, considere os requisitos de interoperabilidade COM em tempo de design. Tipos gerenciados (classe, interface, estrutura e enumeração) se integram perfeitamente com tipos COM quando você obedece às seguintes diretrizes:

  • As classes devem implementar interfaces explicitamente.

    Embora a interoperabilidade COM forneça um mecanismo para gerar automaticamente uma interface contendo todos os membros da classe e os membros da respectiva classe base, é muito melhor fornecer interfaces explícitas. A interface gerada automaticamente é chamada de interface de classe. Para obter diretrizes, confira Apresentando a interface de classe.

    Você pode usar Visual Basic, C# e C++ para incorporar as definições de interface no código, em vez de usar a linguagem IDL ou uma equivalente. Para obter detalhes da sintaxe, consulte a documentação da linguagem.

  • Os tipos gerenciados devem ser públicos.

    Somente os tipos públicos em um assembly são registrados e exportados para a biblioteca de tipos. Como resultado, somente os tipos públicos são visíveis para COM.

    Tipos gerenciados expõem recursos para outro código gerenciado que pode não ser exposto a COM. Por exemplo, campos de constantes, construtores com parâmetros e métodos estáticos não são expostos a clientes COM. Além disso, como o runtime realiza marshaling de dados dentro e fora de um tipo, os dados podem ser copiados ou transformados.

  • Propriedades, métodos, campos e eventos devem ser públicos.

    Membros de tipos públicos também devem ser públicos para ser visíveis para COM. Você pode restringir a visibilidade de um assembly, um tipo público ou membros públicos de um tipo público aplicando o ComVisibleAttribute. Por padrão, todos os membros e tipos públicos são visíveis.

  • Os tipos devem ter um construtor sem parâmetros público para ser ativado no COM.

    Tipos públicos gerenciados são visíveis para o COM. No entanto, sem um construtor sem parâmetros público (um construtor sem argumentos), clientes COM não podem criar o tipo. Clientes COM ainda podem usar o tipo se ele é ativado por outros meios.

  • Os tipos não podem ser abstratos.

    Nem clientes COM, tampouco clientes .NET podem criar tipos abstratos.

Quando exportados para COM, a hierarquia de herança de um tipo gerenciado é nivelada. O controle de versão também difere entre ambientes gerenciados e não gerenciados. Os tipos expostos ao COM não têm as mesmas características de controle de versão de outros tipos gerenciados.

Consumir tipos COM do .NET

Se você pretende consumir tipos COM do .NET e não deseja usar ferramentas como Tlbimp.exe (Importador de Biblioteca de Tipos), siga estas diretrizes:

  • As interfaces devem ter o ComImportAttribute aplicado.
  • As interfaces devem ter o GuidAttribute aplicado com a ID da Interface para a interface COM.
  • As interfaces devem ter o InterfaceTypeAttribute aplicado para especificar o tipo de interface base (IUnknown, IDispatch ou IInspectable).
    • A opção padrão é ter o tipo base de IDispatch e acrescentar os métodos declarados à tabela de funções virtuais esperada para a interface.
    • Somente o .NET Framework dá suporte à especificação de um tipo base IInspectable.

Essas diretrizes fornecem os requisitos mínimos para cenários comuns. Muitas outras opções de personalização existem e são descritas em Aplicando atributos de interoperabilidade.

Definir interfaces COM no .NET

Quando o código .NET tenta chamar um método em um objeto COM por meio de uma interface com o atributo ComImportAttribute, ele precisa criar uma tabela de funções virtuais (também conhecida como vtable ou vftable) para formar a definição do .NET da interface e determinar o código nativo a ser chamado. Esse processo é complexo. Os exemplos a seguir mostram alguns casos simples.

Considere uma interface COM com alguns métodos:

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

Para essa interface, a seguinte tabela descreve seu layout de tabela de funções virtuais:

Slot de tabela de funções virtuais IComInterface Nome do método
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2

Cada método é adicionado à tabela de funções virtuais na ordem em que foi declarado. A ordem específica é definida pelo compilador C++, mas para casos simples sem sobrecargas, a ordem de declaração define a ordem na tabela.

Declare uma interface .NET que corresponda a essa interface da seguinte maneira:

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

O InterfaceTypeAttribute especifica a interface base. Ele fornece algumas opções:

Valor ComInterfaceType Tipo de interface base Comportamento para membros na interface atribuída
InterfaceIsIUnknown IUnknown Primeiro, a tabela de funções virtuais tem os membros de IUnknown, depois os membros dessa interface na ordem de declaração.
InterfaceIsIDispatch IDispatch Os membros não são adicionados à tabela de funções virtuais. Eles só podem ser acessados por meio de IDispatch.
InterfaceIsDual IDispatch Primeiro, a tabela de funções virtuais tem os membros de IDispatch, depois os membros dessa interface na ordem de declaração.
InterfaceIsIInspectable IInspectable Primeiro, a tabela de funções virtuais tem os membros de IInspectable, depois os membros dessa interface na ordem de declaração. Com suporte apenas no .NET Framework.

Herança da interface COM e .NET

O sistema de interoperabilidade COM que usa o ComImportAttribute não interage com a herança da interface, portanto, pode causar um comportamento inesperado, a menos que algumas etapas mitigadoras sejam executadas.

O gerador de origem COM que usa o atributo System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute interage com a herança da interface, portanto, se comporta mais conforme o esperado.

Herança da interface COM no C++

No C++, os desenvolvedores podem declarar interfaces COM derivadas de outras interfaces COM da seguinte maneira:

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

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

Esse estilo de declaração é usado regularmente como um mecanismo para adicionar métodos a objetos COM sem alterar as interfaces existentes, o que seria uma alteração interruptiva. Esse mecanismo de herança resulta nos seguintes layouts de tabela de funções virtuais:

Slot de tabela de funções virtuais IComInterface Nome do método
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
Slot de tabela de funções virtuais IComInterface2 Nome do método
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

Como resultado, é fácil chamar um método definido em IComInterface de um IComInterface2*. Especificamente, chamar um método em uma interface base não requer uma chamada para QueryInterface para obter um ponteiro para a interface base. Além disso, C++ permite uma conversão implícita de IComInterface2* para IComInterface*, que é bem definida e permite evitar chamar um QueryInterface novamente. Como resultado, em C ou C++, você nunca precisará chamar QueryInterface para chegar ao tipo base se não quiser, o que pode permitir algumas melhorias de desempenho.

Observação

As interfaces winRT não seguem esse modelo de herança. Eles são definidos para seguir o mesmo modelo que o modelo de interoperabilidade COM baseado em [ComImport] no .NET.

Herança de interface com ComImportAttribute

No .NET, o código C# que se parece com a herança de interface não é, na verdade, herança de interface. Considere o seguinte código:

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

Este código não diz , "J implementa I". Na verdade, ele diz: "qualquer tipo que implementa J também deve implementar I." Essa diferença leva à decisão de design fundamental que torna a herança de interface na interoperabilidade baseada em ComImportAttribute não ergonômica. As interfaces sempre são consideradas por conta própria; a lista de interfaces base de uma interface não tem impacto em nenhum cálculo para determinar uma tabela de funções virtuais para uma determinada interface .NET.

Como resultado, o equivalente natural do exemplo de interface COM do C++ anterior leva a um layout de tabela de funções virtuais diferente.

Código C#:

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

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

Layouts de tabela de funções virtuais:

Slot de tabela de funções virtuais IComInterface Nome do método
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
Slot de tabela de funções virtuais IComInterface2 Nome do método
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface2::Method3

Como essas tabelas de funções virtuais diferem do exemplo C++, isso levará a sérios problemas em tempo de execução. A definição correta dessas interfaces no .NET com ComImportAttribute é a seguinte:

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

No nível do metadados, IComInterface2 não implementa IComInterface, mas especifica apenas que os implementadores de IComInterface2 também devem implementar IComInterface. Portanto, cada método dos tipos de interface base deve ser redeclarado.

Herança de interface com GeneratedComInterfaceAttribute (.NET 8 ou posterior)

O gerador de origem COM disparado pelo GeneratedComInterfaceAttribute implementa a herança da interface C# como herança de interface COM, portanto, as tabelas de funções virtuais são dispostas conforme o esperado. Se você usar o exemplo anterior, a definição correta dessas interfaces no .NET com System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute será a seguinte:

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

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

Os métodos das interfaces base não precisam ser redeclarados e não devem ser redeclarados. A seguinte tabela descreve as tabelas de funções virtuais resultantes:

Slot de tabela de funções virtuais IComInterface Nome do método
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
Slot de tabela de funções virtuais IComInterface2 Nome do método
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

Como você pode ver, essas tabelas correspondem ao exemplo de C++, portanto, essas interfaces funcionarão corretamente.

Confira também