Procedure consigliate di interoperabilità nativa
.NET offre diversi modi per personalizzare il codice di interoperabilità nativo. Questo articolo include le linee guida seguite dai team di Microsoft .NET per l'interoperabilità nativa.
Indicazioni generali
Le linee guida in questa sezione si applicano a tutti gli scenari di interoperabilità.
- ✔️ USARE
[LibraryImport]
, se possibile, quando la destinazione è .NET 7+.- Esistono casi in cui l’uso di
[DllImport]
è appropriato. Un analizzatore di codice con ID SYSLIB1054 indica quando è questo il caso.
- Esistono casi in cui l’uso di
- ✔️ USARE le stesse convenzioni di denominazione e la stessa combinazione di maiuscole/minuscole per i metodi e i parametri del metodo nativo che si vuole chiamare.
- ✔️ PRENDERE IN CONSIDERAZIONE l'uso delle stesse convenzioni di denominazione e della stessa combinazione di maiuscole/minuscole per i valori costanti.
- ✔️ USARE i tipi .NET più simili al tipo nativo. Ad esempio, in C# usare
uint
quando il tipo nativo èunsigned int
. - ✔️ PREFERIRE struct .NET invece delle classi per esprimere tipi nativi di livello superiore.
- ✔️ DO preferisce usare puntatori a funzione, anziché tipi
Delegate
, quando si passano callback a funzioni non gestite in C#. - ✔️ USARE gli attributi
[In]
e[Out]
per i parametri della matrice. - ✔️ USARE solo gli attributi
[In]
e[Out]
per altri tipi quando il comportamento desiderato è diverso da quello predefinito. - ✔️ PRENDERE IN CONSIDERAZIONE l'uso di System.Buffers.ArrayPool<T> per raggruppare in pool i buffer di matrici nativi.
- ✔️ PRENDERE IN CONSIDERAZIONE il wrapping delle dichiarazioni P/Invoke in una classe con lo stesso nome e combinazione di maiuscole/minuscole della libreria nativa.
- In questo modo gli attributi
[LibraryImport]
0[DllImport]
possono usare la funzionalità del linguaggionameof
C# per passare il nome della libreria nativa e assicurarsi che il nome della libreria nativa non sia stato digitato in modo errato.
- In questo modo gli attributi
- ✔️ Usare handle
SafeHandle
per gestire la durata di oggetti che incapsulano risorse non gestite. Per altre informazioni, vedere Pulizia delle risorse non gestite. - ❌ EVITARE finalizzatori per gestire la durata degli oggetti che incapsulano risorse non gestite. Per altre informazioni, vedere Implementare un metodo Dispose.
Impostazioni dell'attributo LibraryImport
Un analizzatore del codice, con ID SYSLIB1054, consente di usare LibraryImportAttribute
. Nella maggior parte dei casi, l'uso di LibraryImportAttribute
richiede una dichiarazione esplicita anziché basarsi sulle impostazioni predefinite. Questa progettazione è intenzionale e consente di evitare comportamenti imprevisti in scenari di interoperabilità.
Impostazioni degli attributi DllImport
Impostazione | Default | Elemento consigliato | Dettagli |
---|---|---|---|
PreserveSig | true |
Mantenere l'impostazione predefinita | Con l'impostazione esplicita su false, i valori restituiti HRESULT di errore verranno convertiti in eccezioni e il valore restituito nella definizione diventa Null di conseguenza. |
SetLastError | false |
Dipende dall'API | Impostare su true se l'API usa GetLastError e usare Marshal.GetLastWin32Error per ottenere il valore. Se l'API imposta una condizione che indica la presenza di un errore, recuperare l'errore prima di effettuare altre chiamate in modo da evitare di sovrascriverlo inavvertitamente. |
CharSet | Definita dal compilatore (specificata nella documentazione del set di caratteri) | Usare in modo esplicito CharSet.Unicode o CharSet.Ansi quando sono presenti stringhe o caratteri nella definizione |
Specifica il comportamento di marshalling delle stringhe e cosa fa ExactSpelling quando l'impostazione è false . Si noti che CharSet.Ansi è in effetti UTF8 su Unix. Nella maggior parte dei casi Windows usa Unicode, mentre Unix usa UTF8. Vedere altre informazioni nella documentazione sui set di caratteri. |
ExactSpelling | false |
true |
Impostare su true e ottenere un leggero miglioramento delle prestazioni perché il runtime non cercherà nomi di funzioni alternativi con suffisso "A" o "W" in base al valore dell'impostazione CharSet ("A" per CharSet.Ansi e "W" per CharSet.Unicode ). |
Parametri stringa
Un string
viene aggiunto e usato direttamente dal codice nativo (anziché copiato) quando viene passato per valore (non ref
o out
) e uno dei seguenti:
- LibraryImportAttribute.StringMarshalling viene definito come Utf16.
- L'argomento viene contrassegnato in modo esplicito come
[MarshalAs(UnmanagedType.LPWSTR)]
. - DllImportAttribute.CharSet è .Unicode
❌ NON usare parametri [Out] string
. I parametri stringa passati per valore con l'attributo [Out]
possono destabilizzare il runtime se la stringa è una stringa centralizzata. Altre informazioni sulla centralizzazione delle stringhe sono disponibili nella documentazione relativa a String.Intern.
✔️ PROVARE a usare matrici char[]
o byte[]
di un pool ArrayPool
quando il codice nativo deve riempire un buffer di caratteri. Per questo è necessario passare l'argomento come [Out]
.
Linee guida specifiche di DllImport
✔️ PROVARE a impostare la proprietà CharSet
in [DllImport]
in modo che il runtime conosca la codifica di stringa prevista.
✔️ PROVARE a evitare i parametri StringBuilder
. Il marshalling di StringBuilder
crea sempre una copia del buffer nativo. Di conseguenza, può risultare estremamente inefficiente. Si consideri lo scenario tipico di chiamata di un'API di Windows che accetta una stringa:
- Creare un parametro
StringBuilder
con la capacità desiderata (alloca la capacità gestita) {1}. - Invoke:
- Alloca un buffer nativo {2}.
- Copia il contenuto, se
[In]
(valore predefinito per un parametroStringBuilder
) - Copia il buffer nativo in una nuova matrice gestita allocata se
[Out]
{3} (valore predefinito anche perStringBuilder
).
ToString()
alloca ancora un'altra matrice gestita {4}.
Si tratta di {4} allocazioni per ottenere una stringa dal codice nativo. La soluzione migliore per limitare questo problema consiste nel riutilizzare StringBuilder
in un'altra chiamata, ma in questo modo si risparmia solo un'allocazione. È preferibile usare e memorizzare nella cache un buffer di caratteri da ArrayPool
. In questo modo si può arrivare alla sola allocazione per ToString()
nelle chiamate successive.
L'altro problema con StringBuilder
è che copia sempre il buffer restituito fino primo valore Null. Se la stringa passata non è terminata o è una stringa con terminazione Null doppia, nel migliore dei casi P/Invoke non è corretto.
Se si usaStringBuilder
, un altro aspetto da tenere presente è che la capacità non include un valore Null nascosto, sempre considerato per l'interoperabilità. È comune sbagliarsi, perché la maggior parte delle API vuole le dimensioni del buffer comprensive del valore Null. Ciò può comportare allocazioni sprecate/superflue. Inoltre, questo problema impedisce al runtime di ottimizzare il marshalling di StringBuilder
per ridurre al minimo le copie.
Per altre informazioni sul marshalling delle stringhe, vedere Marshalling predefinito per le stringhe e Personalizzazione dei parametri stringa.
Specifico per Windows Per le stringhe
[Out]
CLR useràCoTaskMemFree
per impostazione predefinita per liberare le stringhe oSysStringFree
per le stringhe contrassegnate comeUnmanagedType.BSTR
. Per la maggior parte delle API con un buffer di stringhe di output: il conteggio dei caratteri passato deve includere il carattere null. Se il valore restituito è minore del numero di caratteri passato, la chiamata ha avuto esito positivo e il valore è il numero di caratteri senza il carattere Null finale. In caso contrario, il numero corrisponde alle dimensioni richieste del buffer incluso il carattere Null.
- Si passa 5 e si ottiene 4: la stringa è lunga 4 caratteri con un carattere null finale.
- Si passa 5 e si ottiene 6: la stringa è lunga 5 caratteri ed è necessario un buffer di 6 caratteri per contenere il carattere null. Tipi di dati di Windows per le stringhe
Parametri e campi booleani
È facile sbagliare con i valori booleani. Per impostazione predefinita, per il tipo bool
.NET viene effettuato il marshalling nel tipo BOOL
Windows, in cui è un valore a 4 byte. Tuttavia, i tipi _Bool
e bool
in C e C++ sono a byte singolo. A causa di questa differenza può essere difficile risolvere eventuali bug, perché metà del valore restituito verrà rimosso e il risultato verrà modificato solo potenzialmente. Per altre informazioni sul marshalling dei valori bool
.NET nei tipi bool
C o C++, vedere la documentazione relativa alla personalizzazione del marshalling di campi booleani.
GUID
I GUID possono essere usati direttamente nelle firme. Molte API di Windows accettano alias del tipo GUID&
come REFIID
. Quando la firma del metodo contiene un parametro di riferimento, inserire una parola chiave ref
o un attributo [MarshalAs(UnmanagedType.LPStruct)]
nella dichiarazione del parametro GUID.
GUID | GUID per riferimento |
---|---|
KNOWNFOLDERID |
REFKNOWNFOLDERID |
❌ NON usare [MarshalAs(UnmanagedType.LPStruct)]
per parametri GUID diversi da ref
.
Tipi copiabili da BLT
I tipi copiabili da BLT sono tipi che hanno la stessa rappresentazione a livello di bit nel codice gestito e nativo. Di conseguenza non devono essere convertiti in un altro formato per effettuarne il marshalling in e da codice nativo e dovrebbero essere preferiti perché ciò migliora le prestazioni. Alcuni tipi non sono copiabili da BLT, ma sono noti per includere contenuti copiabili da BLT. Questi tipi presentano ottimizzazioni simili a quelle dei tipi copiabili da BLT quando non sono contenuti in un altro tipo, ma non vengono considerati copiabili da BLT quando si trovano in campi di struct o ai fini di UnmanagedCallersOnlyAttribute
.
Tipi copiabili da BLT quando è abilitato il marshalling di runtime
Tipi copiabili da BLT:
byte
,sbyte
,short
,ushort
,int
,uint
,long
,ulong
,single
,double
- Struct e classi con layout fisso che hanno solo tipi valore copiabili da BLT per i campi di istanza
- Per il layout fisso è richiesto
[StructLayout(LayoutKind.Sequential)]
o[StructLayout(LayoutKind.Explicit)]
- Gli struct sono
LayoutKind.Sequential
per impostazione predefinita
- Per il layout fisso è richiesto
Tipi con contenuto copiabile da BLT:
- Matrici unidimensionali non annidate di tipi copiabili da BLT (ad esempio,
int[]
) - Classi con layout fisso che hanno solo tipi valore copiabili da BLT per i campi di istanza
- Per il layout fisso è richiesto
[StructLayout(LayoutKind.Sequential)]
o[StructLayout(LayoutKind.Explicit)]
- Le classi sono
LayoutKind.Auto
per impostazione predefinita
- Per il layout fisso è richiesto
NON copiabili da BLT:
bool
A VOLTE copiabili da BLT:
char
Tipi con contenuto talvolta copiabile da BLT:
string
Quando i tipi copiabili da BLT vengono passati per riferimento con in
, ref
o out
oppure quando i tipi con contenuto copiabile da BLT vengono passati per valore, vengono semplicemente aggiunti dal marshaller invece di essere copiati in un buffer intermedio.
char
è copiabile da BLT in una matrice unidimensionale oppure se fa parte di un tipo che lo contiene viene contrassegnato in modo esplicito con [StructLayout]
con CharSet = CharSet.Unicode
.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct UnicodeCharStruct
{
public char c;
}
string
contiene contenuto copiabile blttable se non è contenuto in un altro tipo e viene passato per valore (non ref
o out
) come argomento e uno dei seguenti:
- StringMarshalling viene definito come Utf16.
- L'argomento viene contrassegnato in modo esplicito come
[MarshalAs(UnmanagedType.LPWSTR)]
. - CharSet è Unicode.
Per verificare se un tipo è copiabile da BLT o contiene contenuti copiabili da BLT, provare a creare un elemento GCHandle
aggiunto. Se il tipo non è una stringa o non è considerato copiabile da BLT, GCHandle.Alloc
genererà una ArgumentException
.
Tipi copiabili da BLT quando il marshalling di runtime è disabilitato
Quando il marshalling di runtime è disabilitato, le regole per le quali i tipi sono copiabili da BLT sono notevolmente più semplici. Tutti i tipi che sono tipi unmanaged
C# e non includono campi contrassegnati con [StructLayout(LayoutKind.Auto)]
sono copiabili da BLT. Tutti i tipi che non sono tipi C# unmanaged
non sono copiabili da BLT. Il concetto di tipi con contenuto copiabile da BLT, ad esempio matrici o stringhe, non si applica quando il marshalling di runtime è disabilitato. Qualsiasi tipo non considerato copiabile da BLT dalla regola menzionata in precedenza non è supportato quando il marshalling di runtime è disabilitato.
Queste regole si discostano dal sistema predefinito principalmente nelle situazioni in cui vengono usati bool
e char
. Quando il marshalling è disabilitato, bool
viene passato come valore a 1 byte e non normalizzato, mentre char
viene sempre passato come valore a 2 byte. Quando il marshalling di runtime è abilitato, è possibile eseguire il mapping di bool
a un valore a 1, 2 o 4 byte e viene sempre normalizzato, mentre viene eseguito il mapping di char
a un valore a 1 o 2 byte a seconda di CharSet
.
✔️ RENDERE le strutture copiabili da BLT quando possibile.
Per altre informazioni, vedi:
- Blittable and Non-Blittable Types (Tipi copiabili da BLT e non copiabili da BLT)
- Marshalling dei tipi
Mantenere attivi gli oggetti gestiti
GC.KeepAlive()
garantisce che un oggetto rimanga nell'ambito fino a quando non viene raggiunto il metodo KeepAlive.
HandleRef
consente al gestore di marshalling mantenere attivo un oggetto per la durata di P/Invoke. Può essere usato al posto di IntPtr
nelle firme dei metodi. SafeHandle
sostituisce questa classe in modo efficace ed è consigliabile usarlo in alternativa.
GCHandle
consente di bloccare un oggetto gestito e di ottenere il puntatore nativo a tale oggetto. Il modello di base è:
GCHandle handle = GCHandle.Alloc(obj, GCHandleType.Pinned);
IntPtr ptr = handle.AddrOfPinnedObject();
handle.Free();
L'aggiunta non è il comportamento predefinito per GCHandle
. L'altro modello principale è per il passaggio di un riferimento a un oggetto gestito tramite codice nativo per tornare poi al codice gestito, in genere con un callback. Ecco il modello:
GCHandle handle = GCHandle.Alloc(obj);
SomeNativeEnumerator(callbackDelegate, GCHandle.ToIntPtr(handle));
// In the callback
GCHandle handle = GCHandle.FromIntPtr(param);
object managedObject = handle.Target;
// After the last callback
handle.Free();
Non dimenticare che GCHandle
deve essere liberato in modo esplicito per evitare perdite di memoria.
Tipi di dati Windows comuni
L'elenco seguente contiene i tipi di dati comunemente usati nelle API Windows e i tipi C# da usare per chiamate nel codice Windows.
I tipi seguenti hanno le stesse dimensioni in Windows a 32 e 64 bit, nonostante i nomi.
Larghezza | Windows | C# | Alternativa |
---|---|---|---|
32 | BOOL |
int |
bool |
8 | BOOLEAN |
byte |
[MarshalAs(UnmanagedType.U1)] bool |
8 | BYTE |
byte |
|
8 | UCHAR |
byte |
|
8 | UINT8 |
byte |
|
8 | CCHAR |
byte |
|
8 | CHAR |
sbyte |
|
8 | CHAR |
sbyte |
|
8 | INT8 |
sbyte |
|
16 | CSHORT |
short |
|
16 | INT16 |
short |
|
16 | SHORT |
short |
|
16 | ATOM |
ushort |
|
16 | UINT16 |
ushort |
|
16 | USHORT |
ushort |
|
16 | WORD |
ushort |
|
32 | INT |
int |
|
32 | INT32 |
int |
|
32 | LONG |
int |
Controllare CLong e CULong . |
32 | LONG32 |
int |
|
32 | CLONG |
uint |
Controllare CLong e CULong . |
32 | DWORD |
uint |
Controllare CLong e CULong . |
32 | DWORD32 |
uint |
|
32 | UINT |
uint |
|
32 | UINT32 |
uint |
|
32 | ULONG |
uint |
Controllare CLong e CULong . |
32 | ULONG32 |
uint |
|
64 | INT64 |
long |
|
64 | LARGE_INTEGER |
long |
|
64 | LONG64 |
long |
|
64 | LONGLONG |
long |
|
64 | QWORD |
long |
|
64 | DWORD64 |
ulong |
|
64 | UINT64 |
ulong |
|
64 | ULONG64 |
ulong |
|
64 | ULONGLONG |
ulong |
|
64 | ULARGE_INTEGER |
ulong |
|
32 | HRESULT |
int |
|
32 | NTSTATUS |
int |
I tipi seguenti, essendo puntatori, seguono la larghezza della piattaforma. Usare IntPtr
/UIntPtr
per questi tipi.
Tipi di puntatore con segno (usare IntPtr ) |
Tipi di puntatore senza segno (usare UIntPtr ) |
---|---|
HANDLE |
WPARAM |
HWND |
UINT_PTR |
HINSTANCE |
ULONG_PTR |
LPARAM |
SIZE_T |
LRESULT |
|
LONG_PTR |
|
INT_PTR |
È possibile effettuare il marshalling di PVOID
Windows, corrispondente a void*
in C, come IntPtr
oppure UIntPtr
, ma preferire void*
quando possibile.
Tipi supportati predefiniti in precedenza
In alcuni rari casi il supporto predefinito per un tipo è stato rimosso.
Il supporto del marshalling predefinito di UnmanagedType.HString
e UnmanagedType.IInspectable
è stato rimosso nella versione .NET 5. È necessario ricompilare i file binari che usano questo tipo di marshalling e che sono destinati a un framework precedente. È comunque possibile effettuare il marshalling di questo tipo, ma è necessario eseguire questa operazione manualmente, come illustrato nell'esempio di codice seguente. Questo codice funzionerà anche in futuro ed è compatibile con i framework precedenti.
public sealed class HStringMarshaler : ICustomMarshaler
{
public static readonly HStringMarshaler Instance = new HStringMarshaler();
public static ICustomMarshaler GetInstance(string _) => Instance;
public void CleanUpManagedData(object ManagedObj) { }
public void CleanUpNativeData(IntPtr pNativeData)
{
if (pNativeData != IntPtr.Zero)
{
Marshal.ThrowExceptionForHR(WindowsDeleteString(pNativeData));
}
}
public int GetNativeDataSize() => -1;
public IntPtr MarshalManagedToNative(object ManagedObj)
{
if (ManagedObj is null)
return IntPtr.Zero;
var str = (string)ManagedObj;
Marshal.ThrowExceptionForHR(WindowsCreateString(str, str.Length, out var ptr));
return ptr;
}
public object MarshalNativeToManaged(IntPtr pNativeData)
{
if (pNativeData == IntPtr.Zero)
return null;
var ptr = WindowsGetStringRawBuffer(pNativeData, out var length);
if (ptr == IntPtr.Zero)
return null;
if (length == 0)
return string.Empty;
return Marshal.PtrToStringUni(ptr, length);
}
[DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern int WindowsCreateString([MarshalAs(UnmanagedType.LPWStr)] string sourceString, int length, out IntPtr hstring);
[DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern int WindowsDeleteString(IntPtr hstring);
[DllImport("api-ms-win-core-winrt-string-l1-1-0.dll")]
[DefaultDllImportSearchPaths(DllImportSearchPath.System32)]
private static extern IntPtr WindowsGetStringRawBuffer(IntPtr hstring, out int length);
}
// Example usage:
[DllImport("api-ms-win-core-winrt-l1-1-0.dll", PreserveSig = true)]
internal static extern int RoGetActivationFactory(
/*[MarshalAs(UnmanagedType.HString)]*/[MarshalAs(UnmanagedType.CustomMarshaler, MarshalTypeRef = typeof(HStringMarshaler))] string activatableClassId,
[In] ref Guid iid,
[Out, MarshalAs(UnmanagedType.IUnknown)] out object factory);
Considerazioni sul tipo di dati multipiattaforma
Nel linguaggio C/C++ esistono tipi che presentano una certa libertà di definizione. Quando si scrive codice per l'interoperabilità multipiattaforma, possono verificarsi casi in cui le piattaforme sono diverse e possono causare problemi se non vengono prese in considerazione.
long
in C/C++
Le dimensioni non sono necessariamente uguali per long
in C/C++ e long
in C#.
In base alla definizione, il tipo long
in C/C++ deve avere "almeno 32" bit. Questo significa che è previsto un numero minimo di bit obbligatori, ma, se necessario, le piattaforme possono scegliere di usarne un numero maggiore. La tabella seguente illustra le differenze tra le piattaforme in termini di bit forniti per il tipo di dati long
in C/C++.
Piattaforma | 32 bit | 64 bit |
---|---|---|
Windows | 32 | 32 |
macOS/*nix | 32 | 64 |
Al contrario, long
in C# è sempre a 64 bit. Per questo motivo, è consigliabile evitare di usare long
in C# per interagire con long
in C/C++.
Questo problema con long
in C/C++ non esiste per char
, short
, int
e long long
in C/C++ in quanto sono rispettivamente a 8, 16, 32 e 64 bit in tutte queste piattaforme.
In .NET 6 e versioni successive usare i tipi CLong
e CULong
per l'interoperabilità con i tipi di dati long
e unsigned long
in C/C++. L'esempio seguente è relativo a CLong
, ma è possibile usare CULong
per astrarre unsigned long
in modo simile.
// Cross platform C function
// long Function(long a);
[DllImport("NativeLib")]
extern static CLong Function(CLong a);
// Usage
nint result = Function(new CLong(10)).Value;
Quando la destinazione è .NET 5 e versioni precedenti, per gestire il problema è necessario dichiarare firme Windows e non Windows separate.
static readonly bool IsWindows = RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
// Cross platform C function
// long Function(long a);
[DllImport("NativeLib", EntryPoint = "Function")]
extern static int FunctionWindows(int a);
[DllImport("NativeLib", EntryPoint = "Function")]
extern static nint FunctionUnix(nint a);
// Usage
nint result;
if (IsWindows)
{
result = FunctionWindows(10);
}
else
{
result = FunctionUnix(10);
}
Struct
Gli struct gestiti vengono creati nello stack e non vengono rimossi fino a quando il metodo non restituisce il controllo. Per definizione vengono quindi "bloccati" (non verranno spostati dal GC). È possibile semplicemente accettare l'indirizzo nei blocchi di codice non gestito, se il codice nativo non userà il puntatore oltre la fine del metodo corrente.
Gli struct copiabili da BLT offrono prestazioni molto migliori, perché possono essere semplicemente usati direttamente dal livello di marshalling. Provare a rendere gli struct copiabili da BLT (ad esempio, evitare bool
). Per altre informazioni, vedere la sezione Tipi copiabili da BLT.
Se lo struct è copiabile da BLT, usare sizeof()
invece di Marshal.SizeOf<MyStruct>()
per ottenere prestazioni migliori. Come indicato in precedenza, è possibile verificare che il tipo sia copiabile da BLT tentando di creare un GCHandle
bloccato. Se il tipo non è una stringa o non è considerato copiabile da BLT, GCHandle.Alloc
genererà una ArgumentException
.
I puntatori agli struct nelle definizioni devono essere passati per ref
oppure usare unsafe
e *
.
✔️ DEFINIRE lo struct gestito nel modo più possibile corrispondente alla forma e ai nomi usati nella documentazione o nell'intestazione ufficiale della piattaforma.
✔️ USARE sizeof()
in C# invece di Marshal.SizeOf<MyStruct>()
per le strutture copiabili da BLT per migliorare le prestazioni.
❌ EVITARE di usare classi per esprimere tipi nativi complessi tramite ereditarietà.
❌ EVITARE di usare campi System.Delegate
o System.MulticastDelegate
per rappresentare i campi del puntatore a funzione nelle strutture.
Poiché System.Delegate e System.MulticastDelegate non hanno una firma obbligatoria, non garantiscono che il delegato passato corrisponda alla firma prevista dal codice nativo. Inoltre, in .NET Framework e .NET Core il marshalling di uno struct contenente System.Delegate
o System.MulticastDelegate
dalla relativa rappresentazione nativa in un oggetto gestito può destabilizzare il runtime se il valore del campo nella rappresentazione nativa non è un puntatore a funzione che esegue il wrapping di un delegato gestito. In .NET 5 e versioni successive il marshalling di un campo System.Delegate
o System.MulticastDelegate
da una rappresentazione nativa a un oggetto gestito non è supportato. Usare un tipo delegato specifico invece di System.Delegate
o System.MulticastDelegate
.
Buffer fissi
È necessario effettuare il marshalling di una matrice come INT_PTR Reserved1[2]
in due campi di IntPtr
, ovvero Reserved1a
e Reserved1b
. Quando la matrice nativa è un tipo primitivo, è possibile usare la parola chiave fixed
per scriverla in modo un po' più pulito. Ad esempio, SYSTEM_PROCESS_INFORMATION
ha un aspetto simile al seguente nell'intestazione nativa:
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
UNICODE_STRING ImageName;
...
} SYSTEM_PROCESS_INFORMATION
In C# è possibile scriverla come segue:
internal unsafe struct SYSTEM_PROCESS_INFORMATION
{
internal uint NextEntryOffset;
internal uint NumberOfThreads;
private fixed byte Reserved1[48];
internal Interop.UNICODE_STRING ImageName;
...
}
Esistono tuttavia alcune complicazioni con i buffer fissi. I buffer fissi di tipi non copiabili da BLT non verranno correttamente sottoposti a marshalling, quindi la matrice sul posto deve essere espansa in più campi singoli. Inoltre, in .NET Framework e .NET Core prima della versione 3.0, se uno struct contenente un campo buffer fisso viene annidato all'interno di uno struct non copiabili da BLT, il campo buffer fisso non verrà correttamente sottoposto a marshalling in codice nativo.