Condividi tramite


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.
  • ✔️ 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 linguaggio nameofC# per passare il nome della libreria nativa e assicurarsi che il nome della libreria nativa non sia stato digitato in modo errato.
  • ✔️ 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:

❌ 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:

  1. Creare un parametro StringBuilder con la capacità desiderata (alloca la capacità gestita) {1}.
  2. Invoke:
    1. Alloca un buffer nativo {2}.
    2. Copia il contenuto, se [In] (valore predefinito per un parametro StringBuilder)
    3. Copia il buffer nativo in una nuova matrice gestita allocata se [Out] {3} (valore predefinito anche per StringBuilder).
  3. 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 o SysStringFree per le stringhe contrassegnate come UnmanagedType.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

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

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:

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 di dati di Windows

Intervalli dei tipi di dati

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.