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 ha un elemento String, mentre nell'ambiente non gestito le stringhe possono essere Unicode ("wide"), non Unicode, con 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 ANSI con terminazione Null, è possibile usare codice simile al seguente:

[DllImport("somenativelibrary.dll")]
static extern int MethodA([MarshalAs(UnmanagedType.LPStr)] 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 oppure char16_t a seconda del CharSet di P/Invoke o della struttura. Vedere la documentazione sui set di caratteri.
System.Char char* oppure char16_t* a seconda del CharSet di P/Invoke o della struttura. 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 LayoutKindSequential 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.

[DllImport("kernel32.dll")]
static extern void GetSystemTime(SystemTime systemTime);

[StructLayout(LayoutKind.Sequential)]
class 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.