Esercitazione: Usare l'API ComWrappers
In questa esercitazione si apprenderà come suddividere correttamente in sottoclassi il tipo ComWrappers
per fornire una soluzione di interoperabilità COM ottimizzata e compatibile con AOT. Prima di iniziare questa esercitazione, è necessario conoscere COM, la relativa architettura e le soluzioni di interoperabilità COM esistenti.
In questa esercitazione verranno implementate le definizioni di interfaccia seguenti. Con queste interfacce e le relative implementazioni verranno illustrati:
- Marshalling e annullamento del marshalling dei tipi oltre il limite COM/.NET.
- Due approcci distinti all'utilizzo di oggetti COM nativi in .NET.
- Schema consigliato per abilitare l'interoperabilità COM personalizzata in .NET 5 e versioni successive.
Tutto il codice sorgente usato in questa esercitazione è disponibile nel repository dotnet/samples.
Nota
In .NET 8 SDK e versioni successive viene fornito un generatore di origini per generare automaticamente un'implementazione di API ComWrappers
. Per altre informazioni, vedere Generazione dell'origine di ComWrappers
.
Definizioni in C#
interface IDemoGetType
{
string? GetString();
}
interface IDemoStoreType
{
void StoreString(int len, string? str);
}
Definizioni in C++ per 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;
};
Panoramica della struttura di ComWrappers
L'API ComWrappers
è stata progettata per fornire l'interazione minima necessaria per l'interoperabilità COM con il runtime .NET 5 e versioni successive. Questo significa che molte delle caratteristiche esistenti nel sistema di interoperabilità COM integrato non sono presenti e devono essere create a partire da blocchi predefiniti di base. Le due responsabilità principali dell'API sono:
- Identificazione efficiente degli oggetti (ad esempio, mapping tra un'istanza di
IUnknown*
e un oggetto gestito). - Interazione con il Garbage Collector (GC).
Per implementare queste caratteristiche, è necessario che la creazione e l'acquisizione del wrapper avvengano tramite l'API ComWrappers
.
Dal momento che le responsabilità dell'API ComWrappers
sono così limitate, la maggior parte del lavoro di interoperabilità deve essere gestita dal consumer. Tuttavia, il lavoro aggiuntivo è in gran parte meccanico e può essere eseguito da una soluzione di generazione dell'origine. Ad esempio, la catena di strumenti C#/WinRT è una soluzione di generazione dell'origine basata su ComWrappers
per fornire il supporto per l'interoperabilità WinRT.
Implementare una sottoclasse ComWrappers
Fornire una sottoclasse ComWrappers
significa fornire al runtime .NET informazioni sufficienti per creare e registrare wrapper per gli oggetti gestiti inseriti in COM e oggetti COM inseriti in .NET. Prima di esaminare una struttura della sottoclasse, è necessario definire alcuni termini.
Wrapper di oggetti gestiti: per poter abilitare l'utilizzo di oggetti .NET da un ambiente non .NET, sono necessari wrapper. Questi wrapper sono storicamente chiamati COM Callable Wrapper (CCW).
Wrapper di oggetti nativi: per poter abilitare l'utilizzo di oggetti COM implementati da un linguaggio non .NET, sono necessari wrapper. Questi wrapper sono storicamente denominati Runtime Callable Wrapper (RCW).
Passaggio 1: Definire i metodi per implementare e comprendere la finalità
Per estendere il tipo di ComWrappers
, è necessario implementare i tre metodi seguenti. Ognuno di questi metodi rappresenta la partecipazione dell'utente alla creazione o all'eliminazione di un tipo di wrapper. I metodi ComputeVtables()
e CreateObject()
creano rispettivamente un wrapper di oggetti gestiti e un wrapper di oggetti nativi. Il metodo ReleaseObjects()
viene usato dal runtime per effettuare una richiesta relativa alla raccolta fornita di wrapper da "rilasciare" dall'oggetto nativo sottostante. Nella maggior parte dei casi il corpo del metodo ReleaseObjects()
può semplicemente generare NotImplementedException, perché viene chiamato solo in uno scenario avanzato che coinvolge il framework di registrazione dei riferimenti.
// 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();
}
Per implementare il metodo ComputeVtables()
, decidere quali tipi gestiti si intende supportare. Per questa esercitazione verranno supportate le due interfacce definite in precedenza (IDemoGetType
e IDemoStoreType
) e un tipo gestito che implementa le due interfacce (DemoImpl
).
class DemoImpl : IDemoGetType, IDemoStoreType
{
string? _string;
public string? GetString() => _string;
public void StoreString(int _, string? str) => _string = str;
}
Per il metodo CreateObject()
, è anche necessario determinare cosa si vuole supportare. In questo caso, tuttavia, si conoscono solo le interfacce COM a cui si è interessati, non le classi COM. Le interfacce utilizzate dal lato COM sono identiche a quelle inserite dal lato .NET ( IDemoGetType
ovvero e IDemoStoreType
).
Il metodo ReleaseObjects()
non verrà implementato in questa esercitazione.
Passaggio 2: Implementare ComputeVtables()
Si inizierà con il wrapper di oggetti gestiti, in quanto tali wrapper sono più semplici. Verrà creata una tabella di metodi virtuale o vtable per ogni interfaccia per inserirli nell'ambiente COM. Per questa esercitazione si definirà una vtable come sequenza di puntatori, in cui ogni puntatore rappresenta un'implementazione di una funzione in un'interfaccia. In questo caso l'ordine è molto importante. In COM ogni interfaccia eredita da IUnknown
. Il tipo IUnknown
include tre metodi definiti nell'ordine seguente: QueryInterface()
, AddRef()
e Release()
. Dopo i metodi IUnknown
vengono i metodi di interfaccia specifici. Considerare, ad esempio, IDemoGetType
e IDemoStoreType
. Concettualmente, le vtable per i tipi sono simili alla seguente:
IDemoGetType | IDemoStoreType
==================================
QueryInterface | QueryInterface
AddRef | AddRef
Release | Release
GetString | StoreString
Esaminando DemoImpl
, è già disponibile un'implementazione per GetString()
e StoreString()
, ma che cosa si può dire sulle funzioni IUnknown
? L'implementazione di un'istanza di IUnknown
esula dallo scopo di questa esercitazione, ma è possibile eseguire manualmente questa operazione in ComWrappers
. In questa esercitazione, tuttavia, si lascerà che sia il runtime a gestire tale parte. È possibile ottenere l'implementazione di IUnknown
usando il metodo ComWrappers.GetIUnknownImpl()
.
Potrebbe sembrare di aver implementato tutti i metodi, ma sfortunatamente in una vtable COM sono utilizzabili solo le funzioni IUnknown
. Poiché COM è esterno al runtime, è necessario creare puntatori a funzioni native all'implementazione di DemoImpl
. A tale scopo, è possibile usare puntatori a funzione C# e UnmanagedCallersOnlyAttribute
. È possibile creare una funzione da inserire nella vtable creando una funzione static
che simula la firma della funzione COM. Di seguito è riportato un esempio della firma COM per IDemoGetType.GetString()
: si ricorda dall'ABI COM che il primo argomento è l'istanza stessa.
[UnmanagedCallersOnly]
public static int GetString(IntPtr _this, IntPtr* str);
L'implementazione wrapper di IDemoGetType.GetString()
deve essere costituita dalla logica di marshalling e quindi da un dispatch all'oggetto gestito di cui viene eseguito il wrapping. Tutto lo stato per il dispatch è contenuto nell'argomento _this
specificato. L'argomento _this
sarà di tipo ComInterfaceDispatch*
. Questo tipo rappresenta una struttura di basso livello con un singolo campo, Vtable
, che verrà illustrato più avanti. Ulteriori dettagli su questo tipo e sul relativo layout sono un dettaglio di implementazione del runtime e non devono essere considerati. Per recuperare l'istanza gestita da un'istanza di ComInterfaceDispatch*
, usare il codice seguente:
IDemoGetType inst = ComInterfaceDispatch.GetInstance<IDemoGetType>((ComInterfaceDispatch*)_this);
Ora che è disponibile un metodo C# che può essere inserito in una vtable, è possibile costruire la vtable. Si noti l'uso di RuntimeHelpers.AllocateTypeAssociatedMemory()
per allocare memoria in modo che funzioni con assembly scaricabili.
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'allocazione delle vtable è la prima parte dell'implementazione di ComputeVtables()
. È anche consigliabile costruire definizioni COM complete per i tipi che si prevede di supportare. Considerare DemoImpl
e le relative parti che dovrebbero essere utilizzabili da COM. Usando le vtable costruite, è ora possibile creare una serie di istanze di ComInterfaceEntry
che rappresentano la visualizzazione completa dell'oggetto gestito in 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'allocazione di elementi vtable e voci per il wrapper di oggetti gestiti può e deve essere eseguita in anticipo perché i dati possono essere usati per tutte le istanze del tipo. In questo caso è possibile eseguire l'operazione in un costruttore static
o in un inizializzatore di modulo, purché venga eseguita in anticipo in modo che il metodo ComputeVtables()
sia il più semplice e rapido possibile.
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;
}
Dopo aver implementato il metodo ComputeVtables()
, la sottoclasse ComWrappers
potrà produrre wrapper di oggetti gestiti per le istanze di DemoImpl
. Tenere presente che il wrapper di oggetti gestiti restituito dalla chiamata a GetOrCreateComInterfaceForObject()
è di tipo IUnknown*
. Se l'API nativa passata al wrapper richiede un'interfaccia diversa, è necessario eseguire un metodo Marshal.QueryInterface()
per tale interfaccia.
var cw = new DemoComWrappers();
var demo = new DemoImpl();
IntPtr ccw = cw.GetOrCreateComInterfaceForObject(demo, CreateComInterfaceFlags.None);
Passaggio 3: Implementare CreateObject()
La costruzione di un wrapper di oggetti nativi offre più opzioni di implementazione e molte più possibilità rispetto alla creazione di un wrapper di oggetti gestiti. La prima domanda da affrontare è quanto la sottoclasse ComWrappers
si dimostrerà permissiva nel supportare i tipi COM. Per supportare tutti i tipi COM, il che è possibile, è necessario scrivere una notevole quantità di codice o ricorrere ad alcuni usi intelligenti di Reflection.Emit
. Per questa esercitazione verranno supportate solo le istanze COM che implementano sia IDemoGetType
che IDemoStoreType
. Dal momento che è nota l'esistenza di un insieme finito e che sono state imposte delle restrizioni sul fatto che qualsiasi istanza COM fornita deve implementare entrambe le interfacce, si potrebbe fornire un unico wrapper definito staticamente; tuttavia, i casi dinamici sono abbastanza comuni in COM, per cui verranno esaminate entrambe le opzioni.
Wrapper di oggetti nativi statico
Verrà esaminata prima l'implementazione statica. Il wrapper di oggetti nativi statico prevede la definizione di un tipo gestito che implementa le interfacce .NET e può inoltrare le chiamate per il tipo gestito all'istanza COM. Di seguito viene fornita una descrizione approssimativa del wrapper statico.
// 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();
}
Per costruire un'istanza di questa classe e fornirla come wrapper, è necessario definire alcuni criteri. Se questo tipo viene usato come wrapper, sembrerebbe che, poiché implementa entrambe le interfacce, anche l'istanza COM sottostante debba implementare entrambe le interfacce. Considerato che si sta adottando questo criterio, è necessario confermarlo tramite chiamate a Marshal.QueryInterface()
nell'istanza 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 di oggetti nativi dinamico
I wrapper dinamici sono più flessibili perché consentono di eseguire query dei tipi in fase di esecuzione anziché in modo statico. Per fornire questo supporto, verrà utilizzato IDynamicInterfaceCastable
. Per altri dettagli, vedere qui. Osservare che DemoNativeDynamicWrapper
implementa solo questa interfaccia. La funzionalità fornita dall'interfaccia è la possibilità di determinare il tipo supportato in fase di esecuzione. L'origine per questa esercitazione esegue un controllo statico durante la creazione, ma solo ai fini della condivisione del codice, perché il controllo potrebbe essere posticipato fino a quando non viene effettuata una chiamata a DemoNativeDynamicWrapper.IsInterfaceImplemented()
.
// 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();
}
Verrà ora esaminata una delle interfacce che DemoNativeDynamicWrapper
supporterà in modo dinamico. Il codice seguente fornisce l'implementazione di IDemoStoreType
usando la funzionalità dei metodi di interfaccia predefiniti.
[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);
}
}
Esistono due aspetti importanti da tenere presenti in questo esempio:
- Attributo
DynamicInterfaceCastableImplementationAttribute
. Questo attributo è obbligatorio per qualsiasi tipo restituito da un metodoIDynamicInterfaceCastable
. Offre l'ulteriore vantaggio di semplificare il trimming IL, il che significa che gli scenari AOT sono più affidabili. - Cast in
DemoNativeDynamicWrapper
. Questo fa parte della natura dinamica diIDynamicInterfaceCastable
. Il tipo restituito daIDynamicInterfaceCastable.GetInterfaceImplementation()
viene usato per "coprire" il tipo che implementaIDynamicInterfaceCastable
. Il succo in questo caso è che il puntatore athis
non è quello che fa finta di essere perché viene consentito un cast daDemoNativeDynamicWrapper
aIDemoStoreTypeNativeWrapper
.
Inoltrare chiamate all'istanza COM
Indipendentemente dal wrapper di oggetti nativi, è necessario poter richiamare funzioni in un'istanza COM. L'implementazione di IDemoStoreTypeNativeWrapper.StoreString()
può essere usata come esempio di uso di puntatori a funzioni 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);
}
}
Verrà ora esaminata la dereferenziazione dell'istanza COM per accedere all'implementazione della relativa vtable. L'ABI COM definisce che il primo puntatore di un oggetto è alla vtable del tipo. Da tale posizione è quindi possibile accedere allo slot desiderato. Si supponga che l'indirizzo dell'oggetto COM sia 0x10000
. Il primo valore con dimensioni del puntatore deve essere l'indirizzo della vtable, in questo esempio 0x20000
. Quando si è nella vtable, si cerca il quarto slot (indice 3 in indicizzazione in base zero) per accedere all'implementazione di StoreString()
.
COM instance
0x10000 0x20000
VTable for IDemoStoreType
0x20000 <Address of QueryInterface>
0x20008 <Address of AddRef>
0x20010 <Address of Release>
0x20018 <Address of StoreString>
La presenza del puntatore a funzione consente quindi di eseguire il dispatch a tale funzione membro per l'oggetto passando l'istanza dell'oggetto come primo parametro. Questo schema dovrebbe essere familiare in base alle definizioni di funzione dell'implementazione del wrapper di oggetti gestiti.
Dopo aver implementato il metodo CreateObject()
, la sottoclasse ComWrappers
potrà produrre wrapper di oggetti nativi per le istanze COM che implementano sia IDemoGetType
che IDemoStoreType
.
IntPtr iunk = ...; // Get a COM instance from native code.
object rcw = cw.GetOrCreateObjectForComInstance(iunk, CreateObjectFlags.UniqueInstance);
Passaggio 4: Gestire i dettagli della durata del wrapper di oggetti nativi
Le implementazioni di ComputeVtables()
e CreateObject()
riguardavano alcuni dettagli sulla durata del wrapper, ma ci sono altre considerazioni da fare. Anche se questo può essere un passaggio breve, può aumentare significativamente la complessità della struttura di ComWrappers
.
A differenza del wrapper di oggetti gestiti, controllato dalle chiamate ai relativi metodi AddRef()
e Release()
, la durata di un wrapper di oggetti nativi è gestita in modo non deterministico dal processo GC. La domanda è: quando il wrapper di oggetti nativi chiama Release()
in IntPtr
che rappresenta l'istanza COM? Esistono due bucket generali:
Il finalizzatore del wrapper di oggetti nativi è responsabile della chiamata del metodo
Release()
dell'istanza COM. Questa è l'unica volta in cui è possibile chiamare questo metodo in modo sicuro. A questo punto, l'operazione di GC ha determinato correttamente che non sono presenti altri riferimenti al wrapper di oggetti nativi nel runtime .NET. In questo caso può esserci una certa complessità se si stanno supportando correttamente Apartments COM. Per altre informazioni, vedere la sezione Considerazioni aggiuntive.Il wrapper di oggetti nativi implementa
IDisposable
e chiamaRelease()
inDispose()
.
Nota
Lo schema di IDisposable
deve essere supportato solo se, durante la chiamata a CreateObject()
, è stato passato il flag CreateObjectFlags.UniqueInstance
. Se questo requisito non viene seguito, è possibile riutilizzare i wrapper di oggetti nativi eliminati dopo l'eliminazione.
Uso della sottoclasse ComWrappers
È ora disponibile una sottoclasse ComWrappers
che può essere testata. Per evitare di creare una libreria nativa che restituisce un'istanza COM che implementa IDemoGetType
e IDemoStoreType
, si userà il wrapper di oggetti gestiti e lo si considererà come un'istanza COM. Questo deve essere possibile per passarlo comunque a COM.
Verrà creato innanzitutto un wrapper di oggetti gestiti. Creare un'istanza di DemoImpl
e visualizzarne lo stato della stringa corrente.
var demo = new DemoImpl();
string? value = demo.GetString();
Console.WriteLine($"Initial string: {value ?? "<null>"}");
A questo punto verrà creata un'istanza di DemoComWrappers
e un wrapper di oggetti gestiti che è possibile passare in un ambiente COM.
var cw = new DemoComWrappers();
IntPtr ccw = cw.GetOrCreateComInterfaceForObject(demo, CreateComInterfaceFlags.None);
Invece di passare il wrapper di oggetti gestiti a un ambiente COM, fingere di aver ricevuto questa istanza COM e quindi creare un wrapper di oggetti nativi per tale ambiente.
var rcw = cw.GetOrCreateObjectForComInstance(ccw, CreateObjectFlags.UniqueInstance);
Con il wrapper di oggetti nativi dovrebbe essere possibile eseguirne il cast in una delle interfacce desiderate e usarlo come oggetto gestito normale. È possibile esaminare l'istanza di DemoImpl
e osservare l'impatto delle operazioni sul wrapper di oggetti nativi che esegue il wrapping di un wrapper di oggetti gestiti che a sua volta esegue il wrapping dell'istanza gestita.
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}");
Poiché la sottoclasse ComWrapper
è stata progettata per supportare CreateObjectFlags.UniqueInstance
, è possibile pulire immediatamente il wrapper di oggetti nativi invece di attendere un'operazione di GC.
(rcw as IDisposable)?.Dispose();
Attivazione COM con ComWrappers
La creazione di oggetti COM viene in genere eseguita tramite l'attivazione COM, uno scenario complesso che esula dallo scopo di questo documento. Per fornire uno schema concettuale da seguire, verrà introdotta l'API CoCreateInstance()
, usata per l'attivazione COM, e verrà illustrato come usarla con ComWrappers
.
Si supponga che l'applicazione contenga il codice C# seguente. Nell'esempio seguente viene usato CoCreateInstance()
per attivare una classe COM e il sistema di interoperabilità COM predefinito per effettuare il marshalling dell'istanza COM all'interfaccia appropriata. Si noti che l'uso di typeof(I).GUID
è limitato a un'asserzione ed è un caso di utilizzo di reflection che può influire se il codice è compatibile con 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 conversione del codice precedente per l'uso di ComWrappers
comporta la rimozione del metodo MarshalAs(UnmanagedType.Interface)
dall'operazione PInvoke CoCreateInstance()
e l'esecuzione manuale del marshalling.
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);
È anche possibile astrarre funzioni di tipo factory come ActivateClass<I>
includendo la logica di attivazione nel costruttore della classe per un wrapper di oggetti nativi. Il costruttore può usare l'API ComWrappers.GetOrRegisterObjectForComInstance()
per associare l'oggetto gestito appena costruito all'istanza COM attivata.
Considerazioni aggiuntive
AOT nativo: la compilazione AOT (Ahead-of-Time) consente di ottimizzare il costo di avvio perché viene evitata la compilazione JIT. La rimozione della compilazione JIT è spesso richiesta anche in alcune piattaforme. Il supporto di AOT è un obiettivo dell'API ComWrappers
, ma qualsiasi implementazione del wrapper deve prestare attenzione a non introdurre inavvertitamente casi in cui la compilazione AOT si interrompe, ad esempio usando la reflection. La proprietà Type.GUID
rappresenta un esempio di utilizzo della reflection, ma in modo non ovvio. La proprietà Type.GUID
usa la reflection per esaminare gli attributi del tipo e quindi potenzialmente il nome e l'assembly contenitore del tipo per generarne il valore.
Generazione dell'origine: la maggior parte del codice necessario per l'interoperabilità COM e un'implementazione di ComWrappers
può probabilmente essere generato automaticamente usando alcuni strumenti. È possibile generare l'origine per entrambi i tipi di wrapper in base alle definizioni COM appropriate, ad esempio libreria dei tipi (TLB), linguaggio di definizione dell'interfaccia (IDL) o assembly di interoperabilità primario.
Registrazione globale: dal momento che l'API ComWrappers
è stata progettata come una nuova fase di interoperabilità COM, è stato necessario fornire una soluzione per integrarla parzialmente nel sistema esistente. Esistono metodi statici che influiscono a livello globale sull'API ComWrappers
e consentono la registrazione di un'istanza globale per supporto di vario tipo. Questi metodi sono progettati per le istanze di ComWrappers
che prevedono di fornire un supporto completo dell'interoperabilità COM in tutti i casi, in modo simile al sistema di interoperabilità COM predefinito.
Supporto per la registrazione dei riferimenti: questo supporto viene usato principalmente per gli scenari WinRT e rappresenta uno scenario avanzato. Per la maggior parte delle implementazioni di ComWrapper
, un flag CreateComInterfaceFlags.TrackerSupport
o CreateObjectFlags.TrackerObject
deve generare un'eccezione NotSupportedException. Se si vuole abilitare questo supporto, ad esempio in una piattaforma Windows o anche non Windows, è consigliabile vedere la catena di strumenti C#/WinRT.
A parte la durata, il sistema di tipi e le funzionalità descritte in precedenza, un'implementazione di ComWrappers
conforme a COM richiede considerazioni aggiuntive. Per qualsiasi implementazione che verrà usata nella piattaforma Windows, è opportuno considerare quanto segue:
Apartments: la struttura organizzativa di COM per il threading è detta "Apartments" prevede regole rigorose che devono essere seguite per garantire la stabilità delle operazioni. Questa esercitazione non implementa wrapper di oggetti nativi che supportano Apartments, ma qualsiasi implementazione pronta per l'ambiente di produzione deve supportare Apartments. A tale scopo, è consigliabile usare l'API
RoGetAgileReference
introdotta in Windows 8. Per le versioni precedenti a Windows 8, prendere in considerazione la tabella dell'interfaccia globale.Sicurezza: COM offre un modello di sicurezza avanzato per l'attivazione delle classi e l'autorizzazione tramite proxy.