Personalizzare il marshalling delle strutture
In alcuni casi le regole di marshalling predefinite per le strutture non sono esattamente quelle necessarie. I runtime .NET offrono alcuni punti di estensione per poter personalizzare il layout della struttura e la modalità di marshalling dei campi. La personalizzazione del layout della struttura è supportata per tutti gli scenari, ma la personalizzazione del marshalling dei campi è supportata solo per gli scenari in cui è abilitato il marshalling di runtime. Se il marshalling di runtime è disabilitato, è necessario eseguire manualmente il marshalling dei campi.
Nota
Questo articolo non illustra la personalizzazione del marshalling per l'interoperabilità generata da codice sorgete. Se si usa l'interoperabilità generata da codice sorgente per P/Invoke o COM, vedere Personalizzazione del marshalling.
Personalizzare il layout della struttura
.NET offre l'attributo System.Runtime.InteropServices.StructLayoutAttribute e l'enumerazione System.Runtime.InteropServices.LayoutKind per consentire di personalizzare il modo in cui i campi vengono inseriti nella memoria. Le linee guida seguenti saranno utili per evitare problemi comuni.
✔️ PRENDERE IN CONSIDERAZIONE l'uso di LayoutKind.Sequential
laddove possibile.
✔️ USARE LayoutKind.Explicit
nel marshalling solo quando lo struct nativo ha anche un layout esplicito, ad esempio un'unione.
❌ EVITARE di usare classi per esprimere tipi nativi complessi tramite ereditarietà.
❌ EVITARE di usare LayoutKind.Explicit
quando si esegue il marshalling delle strutture su piattaforme non Windows se è necessario impostare come destinazione i runtime precedenti a .NET Core 3.0. Il runtime di .NET Core precedente alla versione 3.0 non supporta il passaggio di strutture esplicite per valore a funzioni native in sistemi non Windows Intel o AMD a 64 bit. Tuttavia, il runtime supporta il passaggio di strutture esplicite per riferimento in tutte le piattaforme.
Personalizzazione del marshalling di campi booleani
Il codice nativo include molte rappresentazioni booleane diverse. Solo in Windows esistono tre modi per rappresentare valori booleani. Il runtime non conosce la definizione nativa della struttura, quindi può al massimo fare supposizioni su come effettuare il marshalling dei valori booleani. Il runtime .NET offre un modo per indicare come effettuare il marshalling del campo booleano. Gli esempi seguenti illustrano come eseguire il marshalling del tipo bool
.NET in diversi tipi booleani nativi.
Per impostazione predefinita, il marshalling dei valori booleani viene effettuato come valore BOOL
Win32 a 4 byte nativo, come illustrato nell'esempio seguente:
public struct WinBool
{
public bool b;
}
struct WinBool
{
public BOOL b;
};
Se si vuole essere espliciti, è possibile usare il valore UnmanagedType.Bool per ottenere lo stesso comportamento illustrato in precedenza:
public struct WinBool
{
[MarshalAs(UnmanagedType.Bool)]
public bool b;
}
struct WinBool
{
public BOOL b;
};
Usando i valori UnmanagedType.U1
o UnmanagedType.I1
di seguito, è possibile indicare al runtime di effettuare il marshalling del campo b
come tipo bool
nativo a 1 byte.
public struct CBool
{
[MarshalAs(UnmanagedType.U1)]
public bool b;
}
struct CBool
{
public bool b;
};
In Windows, è possibile usare il valore UnmanagedType.VariantBool per indicare al runtime di effettuare il marshalling del valore booleano in un valore VARIANT_BOOL
a 2 byte:
public struct VariantBool
{
[MarshalAs(UnmanagedType.VariantBool)]
public bool b;
}
struct VariantBool
{
public VARIANT_BOOL b;
};
Nota
VARIANT_BOOL
è diverso dalla maggior parte dei tipi bool in quanto VARIANT_TRUE = -1
e VARIANT_FALSE = 0
. Inoltre, tutti i valori che non sono uguali a VARIANT_TRUE
sono considerati false.
Personalizzazione del marshalling delle matrici
.NET include anche alcuni modi per personalizzare il marshalling delle matrici.
Per impostazione predefinita, .NET effettua il marshalling delle matrici come puntatore a un elenco di elementi contigui:
public struct DefaultArray
{
public int[] values;
}
struct DefaultArray
{
int32_t* values;
};
Per interfacciarsi con le API COM, potrebbe essere necessario effettuare il marshalling delle matrici come oggetti SAFEARRAY*
. È possibile usare System.Runtime.InteropServices.MarshalAsAttribute e il valore UnmanagedType.SafeArray per indicare al runtime di effettuare il marshalling di una matrice come SAFEARRAY*
:
public struct SafeArrayExample
{
[MarshalAs(UnmanagedType.SafeArray)]
public int[] values;
}
struct SafeArrayExample
{
SAFEARRAY* values;
};
Se è necessario personalizzare il tipo di elemento presente in SAFEARRAY
, è possibile usare i campi MarshalAsAttribute.SafeArraySubType e MarshalAsAttribute.SafeArrayUserDefinedSubType per personalizzare il tipo di elemento esatto di SAFEARRAY
.
Se è necessario effettuare il marshalling della matrice sul posto, è possibile usare il valore UnmanagedType.ByValArray per indicare al gestore di marshalling di effettuare il marshalling della matrice sul posto. Quando si usa questo tipo di marshalling, è anche necessario fornire un valore al campo MarshalAsAttribute.SizeConst per il numero di elementi nella matrice, in modo che il runtime possa allocare correttamente spazio per la struttura.
public struct InPlaceArray
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 4)]
public int[] values;
}
struct InPlaceArray
{
int values[4];
};
Nota
.NET non supporta il marshalling di un campo di matrice di lunghezza variabile come membro di matrice flessibile C99.
Personalizzazione del marshalling dei campi stringa
.NET offre anche un'ampia gamma di personalizzazioni per il marshalling dei campi stringa.
Per impostazione predefinita, .NET effettua il marshalling di una stringa come un puntatore a una stringa con terminazione Null. La codifica dipende dal valore del campo StructLayoutAttribute.CharSet in System.Runtime.InteropServices.StructLayoutAttribute. Se non viene specificato alcun attributo, viene usata la codifica predefinita ANSI.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DefaultString
{
public string str;
}
struct DefaultString
{
char* str;
};
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DefaultString
{
public string str;
}
struct DefaultString
{
char16_t* str; // Could also be wchar_t* on Windows.
};
Se è necessario usare codifiche differenti per campi diversi oppure semplicemente si preferisce essere più espliciti nella definizione dello struct, è possibile usare i valori UnmanagedType.LPStr o UnmanagedType.LPWStr per un attributo System.Runtime.InteropServices.MarshalAsAttribute.
public struct AnsiString
{
[MarshalAs(UnmanagedType.LPStr)]
public string str;
}
struct AnsiString
{
char* str;
};
public struct UnicodeString
{
[MarshalAs(UnmanagedType.LPWStr)]
public string str;
}
struct UnicodeString
{
char16_t* str; // Could also be wchar_t* on Windows.
};
Se si vuole effettuare il marshalling delle stringhe con la codifica UTF-8, è possibile usare il valore UnmanagedType.LPUTF8Str in MarshalAsAttribute.
public struct UTF8String
{
[MarshalAs(UnmanagedType.LPUTF8Str)]
public string str;
}
struct UTF8String
{
char* str;
};
Nota
L'uso di UnmanagedType.LPUTF8Str richiede .NET Framework 4.7 (o versioni successive) o .NET Core 1.1 (o versioni successive). Non è disponibile in .NET Standard 2.0.
Se si utilizzano API COM, potrebbe essere necessario effettuare il marshalling di una stringa come BSTR
. Usando il valore UnmanagedType.BStr è possibile effettuare il marshalling di una stringa come BSTR
.
public struct BString
{
[MarshalAs(UnmanagedType.BStr)]
public string str;
}
struct BString
{
BSTR str;
};
Quando si usa un'API basata su WinRT, potrebbe essere necessario effettuare il marshalling di una stringa come HSTRING
. Usando il valore UnmanagedType.HString è possibile effettuare il marshalling di una stringa come HSTRING
. Il marshalling di HSTRING
è supportato solo nei runtime con supporto WinRT predefinito. Il supporto WinRT è stato rimosso in .NET 5, quindi il marshalling di HSTRING
non è supportato in .NET 5 o versioni successive.
public struct HString
{
[MarshalAs(UnmanagedType.HString)]
public string str;
}
struct BString
{
HSTRING str;
};
Se l'API richiede di passare la stringa sul posto nella struttura, è possibile usare il valore UnmanagedType.ByValTStr. Si noti che la codifica per una stringa sottoposta a marshalling con ByValTStr
è determinata dall'attributo CharSet
. È inoltre richiesto il passaggio della lunghezza della stringa con il campo MarshalAsAttribute.SizeConst.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
public struct DefaultString
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
public string str;
}
struct DefaultString
{
char str[4];
};
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct DefaultString
{
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)]
public string str;
}
struct DefaultString
{
char16_t str[4]; // Could also be wchar_t[4] on Windows.
};
Personalizzazione del marshalling dei campi decimali
In Windows è possibile riscontrare alcune API che usano la struttura CY
o CURRENCY
nativa. Per impostazione predefinita, il tipo .NET decimal
effettua il marshalling nella struttura DECIMAL
nativa. Tuttavia, è possibile usare un oggetto MarshalAsAttribute con il valore UnmanagedType.Currency per indicare al gestore di marshalling di convertire un valore decimal
in un valore CY
nativo.
public struct Currency
{
[MarshalAs(UnmanagedType.Currency)]
public decimal dec;
}
struct Currency
{
CY dec;
};
Unioni
Un'unione è un tipo di dati che può contenere tipi diversi di dati nella stessa memoria. Si tratta di un formato comune di dati nel linguaggio C. Un'unione può essere espressa in .NET usando LayoutKind.Explicit
. È consigliabile usare gli struct quando si definisce un'unione in .NET. L'uso delle classi può causare problemi di layout e produrre un comportamento imprevedibile.
struct device1_config
{
void* a;
void* b;
void* c;
};
struct device2_config
{
int32_t a;
int32_t b;
};
struct config
{
int32_t type;
union
{
device1_config dev1;
device2_config dev2;
};
};
public unsafe struct Device1Config
{
void* a;
void* b;
void* c;
}
public struct Device2Config
{
int a;
int b;
}
public struct Config
{
public int Type;
public _Union Anonymous;
[StructLayout(LayoutKind.Explicit)]
public struct _Union
{
[FieldOffset(0)]
public Device1Config Dev1;
[FieldOffset(0)]
public Device2Config Dev2;
}
}
Eseguire il marshalling di System.Object
In Windows è possibile effettuare il marshalling di campi di tipo object
in codice nativo. È possibile effettuare il marshalling di questi campi in uno di tre tipi:
Per impostazione predefinita, un campo di tipo object
verrà sottoposto a marshalling come IUnknown*
che esegue il wrapping dell'oggetto.
public struct ObjectDefault
{
public object obj;
}
struct ObjectDefault
{
IUnknown* obj;
};
Se si vuole effettuare il marshalling di un campo oggetto come IDispatch*
, aggiungere un MarshalAsAttribute con il valore UnmanagedType.IDispatch.
public struct ObjectDispatch
{
[MarshalAs(UnmanagedType.IDispatch)]
public object obj;
}
struct ObjectDispatch
{
IDispatch* obj;
};
Se si vuole effettuare il marshalling come VARIANT
, aggiungere un MarshalAsAttribute con il valore UnmanagedType.Struct.
public struct ObjectVariant
{
[MarshalAs(UnmanagedType.Struct)]
public object obj;
}
struct ObjectVariant
{
VARIANT obj;
};
La tabella seguente descrive i mapping tra i diversi tipi di runtime del campo obj
e i vari tipi archiviati in un VARIANT
:
Tipo di .NET | Tipo VARIANT |
---|---|
byte |
VT_UI1 |
sbyte |
VT_I1 |
short |
VT_I2 |
ushort |
VT_UI2 |
int |
VT_I4 |
uint |
VT_UI4 |
long |
VT_I8 |
ulong |
VT_UI8 |
float |
VT_R4 |
double |
VT_R8 |
char |
VT_UI2 |
string |
VT_BSTR |
System.Runtime.InteropServices.BStrWrapper |
VT_BSTR |
object |
VT_DISPATCH |
System.Runtime.InteropServices.UnknownWrapper |
VT_UNKNOWN |
System.Runtime.InteropServices.DispatchWrapper |
VT_DISPATCH |
System.Reflection.Missing |
VT_ERROR |
(object)null |
VT_EMPTY |
bool |
VT_BOOL |
System.DateTime |
VT_DATE |
decimal |
VT_DECIMAL |
System.Runtime.InteropServices.CurrencyWrapper |
VT_CURRENCY |
System.DBNull |
VT_NULL |