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à.

  • ✔️ Assicurati di usare [LibraryImport], se possibile, quando miri a .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.
  • ✔️ DO definisce firme P/Invoke e puntatore a funzione che corrispondono agli argomenti della funzione C.
  • ✔️ 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.
  • ✔️ CONSIDERA l'incapsulamento delle dichiarazioni P/Invoke in una classe con lo stesso nome e 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.
  • ✔️ Fai uso di handle SafeHandle per gestire la durata degli oggetti che incapsulano risorse non gestite. Per altre informazioni, vedere Pulizia delle risorse non gestite.
  • ❌ EVITARE l'uso dei finalizzatori per gestire la vita 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, aiuta a guidarti con 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 Predefinito Raccomandazione Dettagli
PreserveSig true Mantenere l'impostazione predefinita Quando viene impostato esplicitamente su false, i valori HRESULT di errore verranno convertiti in eccezioni e il valore restituito nella definizione diventa null come risultato.
SetLastError false Dipende dall'API Imposti questo su true se l'API utilizza GetLastError e usi 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 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 fissato e usato direttamente dal codice nativo (anziché copiato) quando viene passato per valore (non ref o out) e uno dei seguenti criteri è rispettato:

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

✔️ CONSIDERARE di usare matrici char[] o byte[] di un insieme 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 crea StringBuildersempre 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. Evocare:
    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 allocazioni {4} necessarie per ricavare 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. Puoi quindi concentrarti solo sull'allocazione per ToString() nelle chiamate successive.

L'altro problema con StringBuilder è che copia sempre il buffer di ritorno fino al primo valore nullo. Se la stringa passata non è terminata o è una stringa con doppia terminazione null, nel migliore dei casi il tuo P/Invoke è scorretto.

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 del marshalling delle stringhe.

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.
  • Inserendo 5, il risultato è 6: la stringa è lunga 5 caratteri, è necessario un buffer di 6 caratteri per contenere il carattere nulla. Tipi di dati di Windows per le stringhe

Parametri e campi booleani

È facile sbagliare con i valori booleani. Per impostazione predefinita, un bool .NET viene convertito in un BOOL Windows, dove è un valore di 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.

Identificatore Unico Globale (GUID) GUID per riferimento
KNOWNFOLDERID REFKNOWNFOLDERID

❌ NON usare [MarshalAs(UnmanagedType.LPStruct)] per parametri GUID diversi da ref.

Tipi blittabili

I tipi blittabili 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 da e verso codice nativo e, poiché ciò migliora le prestazioni, dovrebbero essere preferiti. Alcuni tipi non sono blittabili, ma sono noti per contenere contenuti blittabili. Questi tipi presentano ottimizzazioni simili a quelle dei tipi blittabili quando non sono contenuti in un altro tipo, ma non vengono considerati blittabili quando si trovano in campi di struct o per scopi legati a UnmanagedCallersOnlyAttribute.

Tipi Blittable quando è abilitato il marshalling di runtime

Tipi copiabili da BLT:

  • byte sbyte, short, , ushort, intuint, long, ulong, , singledouble
  • struct con layout fisso che hanno solo tipi di valore blittable per i campi di istanza
    • Per il layout fisso è richiesto [StructLayout(LayoutKind.Sequential)] o [StructLayout(LayoutKind.Explicit)]
    • Gli struct sono LayoutKind.Sequential di default

Tipi con contenuto blittable:

  • Matrici unidimensionali non annidate di tipi primitivi direttamente copiabili in memoria (ad esempio, int[])
  • Classi con layout fisso che hanno solo tipi di valore blittabili 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 contenuti talvolta direttamente copiabili:

  • string

Quando i tipi blittable vengono passati per riferimento con in, ref o out, oppure quando tipi con contenuti blittable vengono passati per valore, vengono semplicemente fissati dal marshaller invece di essere copiati in un buffer intermedio.

char è blittabile in un array unidimensionale oppure se fa parte di un tipo che viene esplicitamente contrassegnato con [StructLayout] e CharSet = CharSet.Unicode.

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct UnicodeCharStruct
{
    public char c;
}

string contiene contenuti blittable se non è contenuto in un altro tipo e viene passato per valore (non ref o out) come argomento e si verifica alcuna delle seguenti condizioni:

  • StringMarshalling viene definito come Utf16.
  • L'argomento viene contrassegnato in modo esplicito come [MarshalAs(UnmanagedType.LPWSTR)].
  • CharSet è Unicode.

Per verificare se un tipo è blittable o contiene contenuti blittable, provare a creare un elemento pinned GCHandle. Se il tipo non è una stringa o non è considerato direttamente copiabile in memoria, GCHandle.Alloc lancerà un'eccezione ArgumentException.

Tipi blittabili quando il marshalling di runtime è disabilitato

Quando il marshalling di runtime è disabilitato, le regole su quali tipi sono blittabili sono notevolmente più semplici. Tutti i tipi che sono tipi unmanaged C# e non includono campi contrassegnati con [StructLayout(LayoutKind.Auto)] sono blittabili. 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 "blittable" dalla regola menzionata in precedenza non è supportato quando il marshalling del 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, bool può essere mappato a un valore di 1, 2 o 4 byte ed è sempre normalizzato. Inoltre, char si mappa a un valore di 1 o 2 byte a seconda di CharSet.

✔️ Assicurati che le tue strutture siano blittable 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 del marshalling di 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'operazione di fissare non è l'impostazione predefinita 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 Finestre 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 viene rimosso.

Il supporto integrato di marshalling 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.

C/C++ long

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
Finestre 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 risolvere 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);
}

Strutture

Gli struct gestiti vengono creati nello stack e non vengono rimossi fino al termine del metodo. Per definizione vengono quindi "bloccati" (non verranno spostati dal GC). È possibile semplicemente prendere l'indirizzo nei blocchi di codice non sicuro, se il codice nativo non userà il puntatore oltre la fine del metodo corrente.

Le strutture blittable offrono prestazioni molto più elevate, perché possono essere utilizzate direttamente dal livello di marshalling. Cercare di rendere struct blittabili (ad esempio, evitare bool). Per ulteriori informazioni, consultare la sezione Tipi Blittable.

Se lo struct è blittable, usare sizeof() invece di Marshal.SizeOf<MyStruct>() per ottenere prestazioni migliori. Come indicato in precedenza, è possibile verificare che il tipo sia blittable tentando di creare un oggetto bloccato GCHandle. Se il tipo non è una stringa o non è considerato blittabile, GCHandle.Alloc genererà una ArgumentException.

I puntatori agli struct nelle definizioni devono essere passati per ref oppure usare unsafe e *.

✔️ Allineare lo struct gestito quanto più possibile alla forma e ai nomi usati nella documentazione o nell'header ufficiale della piattaforma.

✔️ USARE sizeof() in C# invece di Marshal.SizeOf<MyStruct>() per le strutture blittable per migliorare le prestazioni.

❌ NON dipende dalla rappresentazione interna dei tipi di struct esposti dalle librerie di runtime .NET, a meno che non sia documentato in modo esplicito.

❌ 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 una struct contenente System.Delegate o System.MulticastDelegate dalla relativa rappresentazione nativa a un oggetto gestito può destabilizzare il runtime se il valore del campo nella rappresentazione nativa non è un puntatore a funzione che incapsula 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 blittabili non verranno correttamente sottoposti a marshalling, quindi l'array deve essere espanso in più campi individuali. Inoltre, in .NET Framework e .NET Core prima della versione 3.0, se una struttura contenente un campo buffer fisso viene annidata all'interno di una struttura non blittabile, il campo buffer fisso non verrà correttamente gestito nel marshalling verso il codice nativo.