Qualificare i tipi .NET per l'interoperabilità COM

Esporre i tipi .NET a COM

Se si vuole esporre i tipi contenuti in un assembly alle applicazioni COM, tenere presenti i requisiti di interoperabilità COM in fase di progettazione. Rispettando le linee guida seguenti, è possibile ottenere una facile integrazione tra i tipi gestiti (classi, interfacce, strutture ed enumerazioni) e i tipi COM:

  • Le classi devono implementare le interfacce in modo esplicito.

    Anche se l'interoperabilità COM fornisce un meccanismo per generare automaticamente un'interfaccia contenente tutti i membri della classe e i membri della relativa classe di base, è decisamente preferibile fornire interfacce esplicite. L'interfaccia generata automaticamente è detta interfaccia di classe. Per informazioni, vedere Introduzione all'interfaccia della classe.

    Per incorporare le definizioni di interfaccia nel codice, è possibile usare Visual Basic, C# e C++ invece del linguaggio di definizione dell'interfaccia (IDL, Interface Definition Language) o di soluzioni equivalenti. Per informazioni dettagliate sulla sintassi, vedere la documentazione relativa al linguaggio.

  • I tipi gestiti devono essere pubblici.

    Solo i tipi pubblici di un assembly vengono registrati ed esportati nella libreria dei tipi. Di conseguenza, solo i tipi pubblici sono visibili in COM.

    I tipi gestiti espongono ad altro codice gestito funzionalità che potrebbero non essere esposte a COM. Non vengono ad esempio esposti ai client COM i costruttori con parametri, i metodi statici e i campi costanti. Poiché inoltre il runtime esegue il marshalling dei dati in entrata e in uscita da un tipo, è possibile che i dati vengano copiati o trasformati.

  • Metodi, proprietà, campi ed eventi devono essere pubblici.

    Anche i membri dei tipi pubblici devono essere pubblici per essere visibili in COM. È possibile limitare la visibilità di un assembly, di un tipo pubblico o dei membri pubblici di un tipo pubblico applicando l'oggetto ComVisibleAttribute. Per impostazione predefinita, tutti i membri e i tipi pubblici sono visibili.

  • I tipi devono avere un costruttore senza parametri pubblico per essere attivati da COM.

    I tipi pubblici gestiti sono visibili in COM. Senza un costruttore senza parametri pubblico (un costruttore senza argomenti), tuttavia, i client COM non possono creare il tipo. I client COM possono comunque usare il tipo se viene attivato in un altro modo.

  • I tipi non possono essere astratti.

    Né i client COM né i client .NET possono creare tipi astratti.

Quando si esegue l'esportazione in COM, la gerarchia di ereditarietà di un tipo gestito viene appiattita. Anche il controllo delle versioni varia tra ambiente gestito e ambiente non gestito. I tipi esposti a COM non hanno le stesse caratteristiche di controllo delle versioni degli altri tipi gestiti.

Utilizzare i tipi COM da .NET

Se si intende utilizzare i tipi COM da .NET e non si vogliono usare strumenti come Tlbimp.exe (utilità di importazione della libreria dei tipi), è necessario seguire queste linee guida:

  • Alle interfacce deve essere applicato l'oggetto ComImportAttribute.
  • Alle interfacce deve essere applicato l'oggetto GuidAttribute con l'ID interfaccia per l'interfaccia COM.
  • Alle interfacce devono essere applicato l'oggetto InterfaceTypeAttribute per specificare il tipo di interfaccia di base di questa interfaccia (IUnknown, IDispatcho IInspectable).
    • L'opzione predefinita consiste nell'avere il tipo di base IDispatch e aggiungere i metodi dichiarati alla tabella delle funzioni virtuali prevista per l'interfaccia.
    • Solo .NET Framework supporta la specifica di un tipo di base IInspectable.

Queste linee guida forniscono i requisiti minimi per gli scenari comuni. Esistono molte altre opzioni di personalizzazione e sono descritte in Applicazione di attributi di interoperabilità.

Definire le interfacce COM in .NET

Quando il codice .NET tenta di chiamare un metodo su un oggetto COM tramite un'interfaccia con l'attributo ComImportAttribute, deve creare una tabella di funzioni virtuali (nota anche come vtable o vftable) per formare la definizione .NET dell'interfaccia per determinare il codice nativo da chiamare. Questo processo è complesso. Negli esempi seguenti vengono illustrati alcuni casi semplici.

Si consideri un'interfaccia COM con alcuni metodi:

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

Per questa interfaccia, la tabella seguente descrive il layout della tabella delle funzioni virtuali:

Slot della tabella delle funzioni virtuali IComInterface Nome metodo
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2

Ogni metodo viene aggiunto alla tabella delle funzioni virtuali nell'ordine in cui è stato dichiarato. L'ordine specifico è definito dal compilatore C++, ma per semplici casi senza overload, l'ordine di dichiarazione definisce l'ordine nella tabella.

Dichiarare un'interfaccia .NET corrispondente a questa interfaccia come indicato di seguito:

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

InterfaceTypeAttribute specifica l'interfaccia di base. Fornisce alcune opzioni:

ComInterfaceType Valore Tipo di interfaccia di base Comportamento per i membri nell'interfaccia con attributi
InterfaceIsIUnknown IUnknown La tabella delle funzioni virtuali contiene prima i membri di IUnknown, quindi i membri di questa interfaccia nell'ordine di dichiarazione.
InterfaceIsIDispatch IDispatch I membri non vengono aggiunti alla tabella delle funzioni virtuali. Sono accessibili solo tramite IDispatch.
InterfaceIsDual IDispatch La tabella delle funzioni virtuali contiene prima i membri di IDispatch, quindi i membri di questa interfaccia nell'ordine di dichiarazione.
InterfaceIsIInspectable IInspectable La tabella delle funzioni virtuali contiene prima i membri di IInspectable, quindi i membri di questa interfaccia nell'ordine di dichiarazione. Supportato solo in .NET Framework.

Ereditarietà delle interfacce COM e .NET

Il sistema di interoperabilità COM che usa ComImportAttribute non interagisce con l'ereditarietà delle interfacce, quindi può causare comportamenti imprevisti, a meno che non vengano eseguiti alcuni passaggi di mitigazione.

Il generatore di codice sorgente COM che usa l'attributo System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute interagisce con l'ereditarietà delle interfacce, quindi si comporta come previsto.

Ereditarietà delle interfacce COM in C++

In C++, gli sviluppatori possono dichiarare interfacce COM che derivano da altre interfacce COM come indicato di seguito:

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

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

Questo stile di dichiarazione viene usato regolarmente come meccanismo per aggiungere metodi agli oggetti COM senza modificare le interfacce esistenti, il che rappresenterebbe una modifica che causa un'interruzione. Questo meccanismo di ereditarietà genera i layout delle tabelle delle funzioni virtuali seguenti:

Slot della tabella delle funzioni virtuali IComInterface Nome metodo
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
Slot della tabella delle funzioni virtuali IComInterface2 Nome metodo
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

Di conseguenza, è facile chiamare un metodo definito in IComInterface da un oggetto IComInterface2*. In particolare, la chiamata a un metodo su un'interfaccia di base non richiede una chiamata a QueryInterface per ottenere un puntatore all'interfaccia di base. C++ consente inoltre una conversione implicita da IComInterface2* a IComInterface*, che è ben definita e consente di evitare di chiamare di nuovo un oggetto QueryInterface. Di conseguenza, in C o C++, non è mai necessario chiamare QueryInterface per ottenere il tipo di base se non lo si vuole, il che può consentire alcuni miglioramenti delle prestazioni.

Nota

Le interfacce WinRT non seguono questo modello di ereditarietà. Vengono definite per seguire lo stesso modello del modello di interoperabilità COM basato su [ComImport] in .NET.

Ereditarietà delle interfacce con ComImportAttribute

In .NET il codice C# che è simile all'ereditarietà delle interfacce non è in realtà l'ereditarietà delle interfacce. Osservare il codice seguente:

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

Questo codice non dice "J implementa I". Il codice in realtà dice: "qualsiasi tipo che implementa J deve implementare anche I". Questa differenza porta alla decisione di progettazione fondamentale che rende non ergonomica l'ereditarietà delle interfacce nell'interoperabilità basata su ComImportAttribute. Le interfacce vengono sempre considerate autonomamente; l'elenco di interfacce di base di un'interfaccia non ha alcun impatto sui calcoli per determinare una tabella di funzioni virtuali per una determinata interfaccia .NET.

Di conseguenza, l'equivalente naturale dell'esempio di interfaccia COM C++ precedente porta a un layout diverso della tabella delle funzioni virtuali.

Codice C#:

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

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

Layout delle tabelle delle funzioni virtuali:

Slot della tabella delle funzioni virtuali IComInterface Nome metodo
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
Slot della tabella delle funzioni virtuali IComInterface2 Nome metodo
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface2::Method3

Poiché queste tabelle di funzioni virtuali differiscono dall'esempio C++, questo causerà gravi problemi in fase di esecuzione. La definizione corretta di queste interfacce in .NET con ComImportAttribute è la seguente:

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

A livello di metadati, IComInterface2 non implementa IComInterface ma specifica solo che gli implementatori di IComInterface2 devono implementare anche IComInterface. Pertanto, ogni metodo dei tipi di interfaccia di base deve essere dichiarato nuovamente.

Ereditarietà delle interfacce con GeneratedComInterfaceAttribute (.NET 8 e versioni successive)

Il generatore di codice sorgente COM attivato da GeneratedComInterfaceAttribute implementa l'ereditarietà delle interfacce C# come ereditarietà delle interfacce COM, pertanto le tabelle delle funzioni virtuali vengono disposte come previsto. Se si prende l'esempio precedente, la definizione corretta di queste interfacce in .NET con System.Runtime.InteropServices.Marshalling.GeneratedComInterfaceAttribute è la seguente:

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

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

I metodi delle interfacce di base non devono essere dichiarati nuovamente e non lo saranno. Nella tabella seguente vengono descritte le tabelle delle funzioni virtuali risultanti:

Slot della tabella delle funzioni virtuali IComInterface Nome metodo
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
Slot della tabella delle funzioni virtuali IComInterface2 Nome metodo
0 IUnknown::QueryInterface
1 IUnknown::AddRef
2 IUnknown::Release
3 IComInterface::Method
4 IComInterface::Method2
5 IComInterface2::Method3

Come si può notare, queste tabelle corrispondono all'esempio C++, quindi queste interfacce funzioneranno correttamente.

Vedi anche