Marshalling dei tipi
Il termine marshalling indica il processo di trasformazione dei tipi quando questi devono passare dal codice gestito a quello nativo e viceversa.
Il marshalling è necessario perché i tipi presenti nel codice gestito e quelli presenti nel codice non gestito sono differenti. Nel codice gestito, ad esempio, si dispone di un string
, mentre le stringhe non gestite possono essere .NET string
codifica (UTF-16), codifica della tabella codici ANSI, UTF-8, terminazione null, ASCII e così via. Per impostazione predefinita, il sottosistema di P/Invoke tenta di eseguire le operazioni necessarie in base al comportamento predefinito, descritto in questo articolo. Tuttavia, per i casi in cui è necessario un controllo aggiuntivo, è possibile usare l'attributo MarshalAs per specificare il tipo previsto sul lato non gestito. Ad esempio, se si vuole che la stringa venga inviata come stringa UTF-8 con terminazione Null, è possibile usare codice simile al seguente:
[LibraryImport("somenativelibrary.dll")]
static extern int MethodA([MarshalAs(UnmanagedType.LPStr)] string parameter);
// or
[LibraryImport("somenativelibrary.dll", StringMarshalling = StringMarshalling.Utf8)]
static extern int MethodB(string parameter);
Se si applica l'attributo System.Runtime.CompilerServices.DisableRuntimeMarshallingAttribute
all'assembly, le regole nella sezione seguente non si applicano. Per informazioni su come i valori .NET vengono esposti al codice nativo quando viene applicato questo attributo, vedere Marshalling di runtime disabilitato.
Regole predefinite per il marshalling dei tipi comuni
In genere, il runtime tenta di eseguire la "cosa giusta" quando effettua il marshalling, in modo da richiedere un intervento minimo da parte dell'utente. Le tabelle seguenti descrivono come viene effettuato il marshalling di ogni tipo per impostazione predefinita, quando viene usato in un parametro o un campo. I tipi carattere e integer a larghezza fissa C99/C++11 vengono usati per assicurarsi che la tabella seguente sia corretta per tutte le piattaforme. È possibile usare qualsiasi tipo nativo con gli stessi requisiti di allineamento e dimensioni di questi tipi.
La prima tabella descrive i mapping per i vari tipi per i quali il marshalling è lo stesso sia per P/Invoke che per i campi.
Parola chiave C# | Tipo di .NET | Tipo nativo |
---|---|---|
byte |
System.Byte |
uint8_t |
sbyte |
System.SByte |
int8_t |
short |
System.Int16 |
int16_t |
ushort |
System.UInt16 |
uint16_t |
int |
System.Int32 |
int32_t |
uint |
System.UInt32 |
uint32_t |
long |
System.Int64 |
int64_t |
ulong |
System.UInt64 |
uint64_t |
char |
System.Char |
char o char16_t a seconda della codifica della struttura o P/Invoke. Vedere la documentazione sui set di caratteri. |
System.Char |
char* o char16_t* a seconda della codifica della struttura o P/Invoke. Vedere la documentazione sui set di caratteri. |
|
nint |
System.IntPtr |
intptr_t |
nuint |
System.UIntPtr |
uintptr_t |
Tipi di puntatore .NET (ad esempio void* ) |
void* |
|
Tipo derivato da System.Runtime.InteropServices.SafeHandle |
void* |
|
Tipo derivato da System.Runtime.InteropServices.CriticalHandle |
void* |
|
bool |
System.Boolean |
Tipo BOOL Win32 |
decimal |
System.Decimal |
Struct DECIMAL COM |
Delegato .NET | Puntatore di funzione nativo | |
System.DateTime |
Tipo DATE Win32 |
|
System.Guid |
Tipo GUID Win32 |
Alcune categorie di marshalling hanno impostazioni predefinite diverse se si effettua il marshalling come parametro o come struttura.
Tipo di .NET | Tipo nativo (parametro) | Tipo nativo (campo) |
---|---|---|
Matrice .NET | Un puntatore all'inizio della matrice delle rappresentazioni native degli elementi della matrice. | Non consentito senza un attributo [MarshalAs] |
Una classe con LayoutKind Sequential o Explicit |
Un puntatore alla rappresentazione nativa della classe | La rappresentazione nativa della classe |
La tabella seguente include le regole di marshalling predefinite solo per Windows. Nelle piattaforme non Windows non è possibile effettuare il marshalling di questi tipi.
Tipo di .NET | Tipo nativo (parametro) | Tipo nativo (campo) |
---|---|---|
System.Object |
VARIANT |
IUnknown* |
System.Array |
Interfaccia COM | Non consentito senza un attributo [MarshalAs] |
System.ArgIterator |
va_list |
Non consentito |
System.Collections.IEnumerator |
IEnumVARIANT* |
Non consentito |
System.Collections.IEnumerable |
IDispatch* |
Non consentito |
System.DateTimeOffset |
int64_t che rappresenta il numero di tick dalla mezzanotte del 1° gennaio 1601 |
int64_t che rappresenta il numero di tick dalla mezzanotte del 1° gennaio 1601 |
Per alcuni tipi è possibile effettuare il marshalling solo come parametri e non come campi. Questi tipi sono elencati nella tabella seguente:
Tipo di .NET | Tipo nativo (solo parametro) |
---|---|
System.Text.StringBuilder |
char* oppure char16_t* a seconda del CharSet di P/Invoke. Vedere la documentazione sui set di caratteri. |
System.ArgIterator |
va_list (solo in Windows x86/x64/arm64) |
System.Runtime.InteropServices.ArrayWithOffset |
void* |
System.Runtime.InteropServices.HandleRef |
void* |
Se queste impostazioni predefinite non producono esattamente il risultato desiderato, è possibile personalizzare la modalità di marshalling dei parametri. L'articolo dedicato al marshalling dei parametri illustra in modo dettagliato come personalizzare la modalità di marshalling dei diversi tipi di parametri.
Marshalling predefinito in scenari COM
Quando si chiama un metodo su oggetti COM in .NET, il runtime di .NET cambia le regole del marshalling predefinito in modo da rispettare la semantica COM. Nella tabella seguente sono elencate le regole che i runtime .NET usano negli scenari COM:
Tipo di .NET | Tipo nativo (chiamate al metodo COM) |
---|---|
System.Boolean |
VARIANT_BOOL |
StringBuilder |
LPWSTR |
System.String |
BSTR |
Tipi delegato | _Delegate* in .NET Framework. Non consentito in .NET Core e .NET 5+. |
System.Drawing.Color |
OLECOLOR |
Matrice .NET | SAFEARRAY |
System.String[] |
SAFEARRAY di BSTR |
Marshalling di classi e strutture
Un altro aspetto del marshalling dei tipi è il modo in cui è possibile passare una struttura a un metodo non gestito. Ad esempio, alcuni dei metodi non gestiti richiedono una struttura come parametro. In questi casi, è necessario creare una classe o uno struct corrispondente nella parte gestita per usarlo come parametro. La semplice definizione della classe, tuttavia, non è sufficiente. È necessario anche indicare al gestore di marshalling come eseguire il mapping dei campi della classe allo struct non gestito. In questo caso diventa utile l'attributo StructLayout
.
[LibraryImport("kernel32.dll")]
static partial void GetSystemTime(out SystemTime systemTime);
[StructLayout(LayoutKind.Sequential)]
struct SystemTime
{
public ushort Year;
public ushort Month;
public ushort DayOfWeek;
public ushort Day;
public ushort Hour;
public ushort Minute;
public ushort Second;
public ushort Millisecond;
}
public static void Main(string[] args)
{
SystemTime st = new SystemTime();
GetSystemTime(st);
Console.WriteLine(st.Year);
}
Il codice precedente illustra un semplice esempio di chiamata della funzione GetSystemTime()
. La parte interessante è alla riga 4. L'attributo specifica che i campi della classe devono essere mappati in modo sequenziale allo struct sull'altro lato (non gestito). Questo significa che i nomi dei campi non sono rilevanti, ma che è importante solo l'ordine. È infatti necessario che i campi corrispondano allo struct non gestito, come mostrato nell'esempio seguente:
typedef struct _SYSTEMTIME {
WORD wYear;
WORD wMonth;
WORD wDayOfWeek;
WORD wDay;
WORD wHour;
WORD wMinute;
WORD wSecond;
WORD wMilliseconds;
} SYSTEMTIME, *PSYSTEMTIME;
In alcuni casi il marshalling predefinito per la struttura non produce i risultati previsti. L'articolo Personalizzazione del marshalling delle strutture illustra come personalizzare la modalità di marshalling della struttura.