Práticas recomendadas de interoperabilidade nativa
O .NET oferece várias maneiras de personalizar seu código de interoperabilidade nativo. Este artigo inclui a orientação que as equipes .NET da Microsoft seguem para a interoperabilidade nativa.
Documentação de orientação geral
As orientações nesta secção aplicam-se a todos os cenários de interoperabilidade.
- ✔️ USE
[LibraryImport]
, se possível, ao direcionar o .NET 7+.- Há casos em que o uso
[DllImport]
é apropriado. Um analisador de código com ID SYSLIB1054 informa quando esse é o caso.
- Há casos em que o uso
- ✔️ USE a mesma nomenclatura e capitalização para seus métodos e parâmetros que o método nativo que você deseja chamar.
- ✔️ CONSIDERE usar a mesma nomenclatura e capitalização para valores constantes.
- ✔️ USE tipos .NET que mapeiam mais próximos do tipo nativo. Por exemplo, em C#, use
uint
quando o tipo nativo forunsigned int
. - ✔️ DO prefere expressar tipos nativos de nível superior usando estruturas .NET em vez de classes.
- ✔️ DO prefere usar ponteiros de função, em vez de
Delegate
tipos, ao passar retornos de chamada para funções não gerenciadas em C#. - ✔️ DO use
[In]
e[Out]
atributos em parâmetros de matriz. - ✔️ DO use
[In]
e[Out]
atributos somente em outros tipos quando o comportamento desejado for diferente do comportamento padrão. - ✔️ CONSIDERE usar System.Buffers.ArrayPool<T> para agrupar seus buffers de array nativos.
- ✔️ CONSIDERE envolver suas declarações P/Invoke em uma classe com o mesmo nome e maiúsculas que sua biblioteca nativa.
- Isso permite que seus
[LibraryImport]
atributos ou[DllImport]
usem o recurso de linguagem C#nameof
para passar o nome da biblioteca nativa e garantir que você não tenha escrito incorretamente o nome da biblioteca nativa.
- Isso permite que seus
- ✔️ DO use
SafeHandle
identificadores para gerenciar o tempo de vida de objetos que encapsulam recursos não gerenciados. Para obter mais informações, consulte Limpeza de recursos não gerenciados. - ❌ EVITE finalizadores para gerenciar o tempo de vida de objetos que encapsulam recursos não gerenciados. Para obter mais informações, consulte Implementar um método Dispose.
Configurações do atributo LibraryImport
Um analisador de código, com ID SYSLIB1054, ajuda a guiá-lo com LibraryImportAttribute
o . Na maioria dos casos, o uso de requer uma declaração explícita em vez de depender de LibraryImportAttribute
configurações padrão. Esse design é intencional e ajuda a evitar comportamentos não intencionais em cenários de interoperabilidade.
Configurações de atributo DllImport
Definição | Predefinido | Recomendação | Detalhes |
---|---|---|---|
PreserveSig | true |
Manter o padrão | Quando isso é explicitamente definido como false, os valores de retorno HRESULT com falha serão transformados em exceções (e o valor de retorno na definição se tornará nulo como resultado). |
SetLastError | false |
Depende da API | Defina isso como true se a API usar GetLastError e usar Marshal.GetLastWin32Error para obter o valor. Se a API definir uma condição que diga que tem um erro, obtenha o erro antes de fazer outras chamadas para evitar que ele seja substituído inadvertidamente. |
CharSet | Definido pelo compilador (especificado na documentação do charset) | Usar explicitamente CharSet.Unicode ou CharSet.Ansi quando cadeias de caracteres ou caracteres estiverem presentes na definição |
Isso especifica o comportamento de empacotamento de cadeias de caracteres e o que ExactSpelling faz quando false . Note que CharSet.Ansi na verdade é UTF8 no Unix. Na maioria das vezes, o Windows usa Unicode, enquanto o Unix usa UTF8. Veja mais informações sobre a documentação sobre charsets. |
ExactSpelling | false |
true |
Defina isso como true e obtenha um pequeno benefício perf, pois o tempo de execução não procurará nomes de função alternativos com um sufixo "A" ou "W", dependendo do valor da CharSet configuração ("A" para CharSet.Ansi e "W" para CharSet.Unicode ). |
Parâmetros de cadeia de caracteres
A string
é fixado e usado diretamente pelo código nativo (em vez de copiado) quando passado pelo valor (não ref
ou out
) e qualquer um dos seguintes:
- LibraryImportAttribute.StringMarshalling é definida como Utf16.
- O argumento é explicitamente marcado como
[MarshalAs(UnmanagedType.LPWSTR)]
. - DllImportAttribute.CharSet é Unicode.
❌ NÃO use [Out] string
parâmetros. Os parâmetros de cadeia de caracteres passados pelo valor com o [Out]
atributo podem desestabilizar o tempo de execução se a cadeia de caracteres for uma cadeia de caracteres internada. Consulte mais informações sobre o internamento de cadeias de caracteres na documentação String.Interndo .
✔️ CONSIDERE char[]
ou matrizes de um ArrayPool
código nativo em que se espera que preencha byte[]
um buffer de caracteres. Isso requer passar o argumento como [Out]
.
Orientação específica de DllImport
✔️ CONSIDERE definir a CharSet
propriedade para [DllImport]
que o tempo de execução conheça a codificação de cadeia de caracteres esperada.
✔️ CONSIDERE evitar StringBuilder
parâmetros. StringBuilder
O marshalling sempre cria uma cópia de buffer nativa. Como tal, pode ser extremamente ineficiente. Considere o cenário típico de chamar uma API do Windows que usa uma cadeia de caracteres:
- Crie um
StringBuilder
da capacidade desejada (aloca a capacidade gerenciada) {1}. - Invoque:
- Aloca um buffer {2}nativo .
- Copia o conteúdo if
[In]
(o padrão para umStringBuilder
parâmetro). - Copia o buffer nativo em uma matriz gerenciada recém-alocada se
[Out]
{3} (também o padrão paraStringBuilder
).
ToString()
aloca mais uma matriz {4}gerenciada.
Isso são {4} alocações para obter uma cadeia de caracteres do código nativo. O melhor que você pode fazer para limitar isso é reutilizar o StringBuilder
em outra chamada, mas isso ainda salva apenas uma alocação. É muito melhor usar e armazenar em cache um buffer de caracteres do ArrayPool
. Você pode então chegar apenas à alocação para as ToString()
chamadas subsequentes.
O outro problema é StringBuilder
que ele sempre copia o buffer de retorno de volta para o primeiro nulo. Se a cadeia de caracteres passada não for encerrada ou for uma cadeia de caracteres terminada com duplo nulo, seu P/Invoke está, na melhor das hipóteses, incorreto.
Se você usar StringBuilder
, um último problema é que a capacidade não inclui um nulo oculto, que é sempre contabilizado na interoperabilidade. É comum que as pessoas erram, pois a maioria das APIs quer o tamanho do buffer , incluindo o nulo. Isso pode resultar em alocações desperdiçadas/desnecessárias. Além disso, esse gotcha impede que o tempo de execução otimize StringBuilder
o empacotamento para minimizar cópias.
Para obter mais informações sobre empacotamento de strings, consulte Marshalling padrão para strings e Customizing string marshalling.
Específico do Windows Para
[Out]
cadeias de caracteres que o CLR usaráCoTaskMemFree
por padrão para liberar cadeias de caracteres ouSysStringFree
para cadeias de caracteres marcadas comoUnmanagedType.BSTR
. Para a maioria das APIs com um buffer de cadeia de caracteres de saída: A contagem de caracteres passados deve incluir o nulo. Se o valor retornado for menor que o passado na contagem de caracteres, a chamada foi bem-sucedida e o valor é o número de caracteres sem o nulo à direita. Caso contrário, a contagem é o tamanho necessário do buffer , incluindo o caractere nulo.
- Passe em 5, obtenha 4: A cadeia de caracteres tem 4 caracteres com um nulo à direita.
- Passe em 5, obtenha 6: A cadeia de caracteres tem 5 caracteres, precisa de um buffer de 6 caracteres para manter o nulo. Tipos de dados do Windows para cadeias de caracteres
Parâmetros e campos booleanos
Booleanos são fáceis de bagunçar. Por padrão, um .NET bool
é empacotado para um Windows BOOL
, onde é um valor de 4 bytes. No entanto, o , e bool
os _Bool
tipos em C e C++ são um único byte. Isso pode levar a bugs difíceis de rastrear, pois metade do valor de retorno será descartado, o que só potencialmente mudará o resultado. Para obter mais informações sobre como organizar valores .NET bool
para tipos C ou C++ bool
, consulte a documentação sobre como personalizar o empacotamento de campo booleano.
GUIDs
GUIDs são utilizáveis diretamente em assinaturas. Muitas APIs do Windows usam GUID&
aliases de tipo como REFIID
. Quando a assinatura do método contém um parâmetro de referência, coloque uma ref
palavra-chave ou um [MarshalAs(UnmanagedType.LPStruct)]
atributo na declaração de parâmetro GUID.
GUID | GUID por referência |
---|---|
KNOWNFOLDERID |
REFKNOWNFOLDERID |
❌ NÃO Use [MarshalAs(UnmanagedType.LPStruct)]
para nada além de ref
parâmetros GUID.
Tipos blittable
Os tipos Blittable são tipos que têm a mesma representação de nível de bits em código gerenciado e nativo. Como tal, eles não precisam ser convertidos para outro formato para serem empacotados de e para código nativo e, como isso melhora o desempenho, eles devem ser preferidos. Alguns tipos não são blittable, mas são conhecidos por conter conteúdo blittable. Esses tipos têm otimizações semelhantes aos tipos blittable quando não estão contidos em outro tipo, mas não são considerados blittable quando em campos de structs ou para fins de UnmanagedCallersOnlyAttribute
.
Tipos blittable quando o empacotamento de tempo de execução está habilitado
Tipos litíveis:
byte
,sbyte
, ,short
,ushort
,uint
int
,long
,ulong
single
,double
- structs com layout fixo que só têm tipos de valor blittable para campos de exemplo
- layout fixo requer
[StructLayout(LayoutKind.Sequential)]
ou[StructLayout(LayoutKind.Explicit)]
- structs são
LayoutKind.Sequential
por padrão
- layout fixo requer
Tipos com conteúdo blittable:
- matrizes unidimensionais não aninhadas de tipos primitivos blittable (por exemplo,
int[]
) - classes com layout fixo que só têm tipos de valor blittable para campos de exemplo
- layout fixo requer
[StructLayout(LayoutKind.Sequential)]
ou[StructLayout(LayoutKind.Explicit)]
- As classes são
LayoutKind.Auto
, por padrão,
- layout fixo requer
NÃO blittable:
bool
ÀS VEZES blittable:
char
Tipos com conteúdo ÀS vezes blittable:
string
Quando tipos blittable são passados por referência com in
, ref
ou , ou out
quando tipos com conteúdo blittable são passados por valor, eles são simplesmente fixados pelo marshaller em vez de serem copiados para um buffer intermediário.
char
é blittable em uma matriz unidimensional ou se for parte de um tipo que contém está explicitamente marcado com [StructLayout]
CharSet = CharSet.Unicode
.
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct UnicodeCharStruct
{
public char c;
}
string
contém conteúdo blittable se ele não estiver contido em outro tipo e estiver sendo passado por valor (não ref
ou out
) como um argumento e qualquer um dos seguintes:
- StringMarshalling é definida como Utf16.
- O argumento é explicitamente marcado como
[MarshalAs(UnmanagedType.LPWSTR)]
. - CharSet é Unicode.
Você pode ver se um tipo é blittable ou contém conteúdo blittable tentando criar um fixo GCHandle
. Se o tipo não for uma string ou considerado blittable, GCHandle.Alloc
lançará um ArgumentException
arquivo .
Tipos blittable quando o empacotamento em tempo de execução está desativado
Quando o empacotamento de tempo de execução é desativado, as regras para as quais os tipos são blittable são significativamente mais simples. Todos os tipos que são tipos C# unmanaged
e não têm nenhum campo marcado com [StructLayout(LayoutKind.Auto)]
são blittable. Todos os tipos que não são tipos C# unmanaged
não são blittable. O conceito de tipos com conteúdo blittable, como matrizes ou strings, não se aplica quando a organização em tempo de execução está desabilitada. Qualquer tipo que não seja considerado blittable pela regra acima mencionada não é suportado quando o empacotamento de tempo de execução está desativado.
Essas regras diferem do sistema interno principalmente em situações em que bool
e char
são usadas. Quando a empacotação está desativada, bool
é passada como um valor de 1 byte e não normalizada e char
é sempre passada como um valor de 2 bytes. Quando o empacotamento de tempo de execução está habilitado, bool
pode mapear para um valor de 1, 2 ou 4 bytes e é sempre normalizado, e char
mapeia para um valor de 1 ou 2 bytes, dependendo do CharSet
.
✔️ FAÇA com que as suas estruturas sejam blittable sempre que possível.
Para obter mais informações, consulte:
Mantendo os objetos gerenciados ativos
GC.KeepAlive()
garantirá que um objeto permaneça no escopo até que o método KeepAlive seja atingido.
HandleRef
permite que o marshaller mantenha um objeto vivo durante a duração de um P/Invoke. Ele pode ser usado em vez de em assinaturas de IntPtr
método. SafeHandle
substitui efetivamente esta classe e deve ser usado em vez disso.
GCHandle
Permite fixar um objeto gerenciado e obter o ponteiro nativo para ele. O padrão básico é:
GCHandle handle = GCHandle.Alloc(obj, GCHandleType.Pinned);
IntPtr ptr = handle.AddrOfPinnedObject();
handle.Free();
A fixação não é o padrão do GCHandle
. O outro padrão principal é passar uma referência a um objeto gerenciado por meio de código nativo e voltar para o código gerenciado, geralmente com um retorno de chamada. Aqui está o padrão:
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();
Não se esqueça que GCHandle
precisa ser explicitamente liberado para evitar vazamentos de memória.
Tipos de dados comuns do Windows
Aqui está uma lista de tipos de dados comumente usados em APIs do Windows e quais tipos de C# usar ao chamar o código do Windows.
Os tipos a seguir são do mesmo tamanho no Windows de 32 bits e 64 bits, apesar de seus nomes.
Width | 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 |
Veja CLong e CULong . |
32 | LONG32 |
int |
|
32 | CLONG |
uint |
Veja CLong e CULong . |
32 | DWORD |
uint |
Veja CLong e CULong . |
32 | DWORD32 |
uint |
|
32 | UINT |
uint |
|
32 | UINT32 |
uint |
|
32 | ULONG |
uint |
Veja 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 |
Os seguintes tipos, sendo ponteiros, seguem a largura da plataforma. Use IntPtr
/UIntPtr
para estes.
Tipos de ponteiro assinado (use IntPtr ) |
Tipos de ponteiro não assinados (use UIntPtr ) |
---|---|
HANDLE |
WPARAM |
HWND |
UINT_PTR |
HINSTANCE |
ULONG_PTR |
LPARAM |
SIZE_T |
LRESULT |
|
LONG_PTR |
|
INT_PTR |
Um Windows PVOID
, que é um C void*
, pode ser organizado como um ou IntPtr
UIntPtr
, mas prefere void*
quando possível.
Tipos suportados anteriormente incorporados
Há casos raros em que o suporte interno para um tipo é removido.
O UnmanagedType.HString
suporte a marshal interno foi UnmanagedType.IInspectable
removido na versão .NET 5. Você deve recompilar binários que usam esse tipo de empacotamento e que visam uma estrutura anterior. Ainda é possível organizar esse tipo, mas você deve organizá-lo manualmente, como mostra o exemplo de código a seguir. Este código funcionará no futuro e também é compatível com estruturas anteriores.
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);
Considerações sobre o tipo de dados entre plataformas
Existem tipos na linguagem C/C++ que têm latitude na forma como são definidos. Ao escrever interoperabilidade entre plataformas, podem surgir casos em que as plataformas diferem e podem causar problemas se não forem considerados.
C/C++ long
C/C++ long
e C# long
não são necessariamente do mesmo tamanho.
O long
tipo em C/C++ é definido como tendo "pelo menos 32" bits. Isso significa que há um número mínimo de bits necessários, mas as plataformas podem optar por usar mais bits, se desejado. A tabela a seguir ilustra as diferenças nos bits fornecidos para o tipo de dados C/C++ long
entre plataformas.
Plataforma | 32 bits | 64 bits |
---|---|---|
Windows | 32 | 32 |
macOS/*nix | 32 | 64 |
Em contraste, o C# long
é sempre de 64 bits. Por esse motivo, é melhor evitar o uso de C# long
para interoperabilidade com C/C++ long
.
(Esse problema com C/C++ long
não existe para C/C++ char
, short
, int
, e long long
como eles são 8, 16, 32 e 64 bits respectivamente em todas essas plataformas.)
No .NET 6 e versões posteriores, use os CLong
tipos e CULong
para interoperabilidade com C/C++ long
e unsigned long
tipos de dados. O exemplo a seguir é para CLong
, mas você pode usar CULong
para abstrair unsigned long
de maneira semelhante.
// Cross platform C function
// long Function(long a);
[DllImport("NativeLib")]
extern static CLong Function(CLong a);
// Usage
nint result = Function(new CLong(10)).Value;
Ao direcionar o .NET 5 e versões anteriores, você deve declarar assinaturas separadas do Windows e não do Windows para lidar com o problema.
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);
}
Estruturas
As estruturas gerenciadas são criadas na pilha e não são removidas até que o método retorne. Por definição, então, eles são "fixados" (não serão movidos pelo GC). Você também pode simplesmente pegar o endereço em blocos de código não seguros se o código nativo não usar o ponteiro após o final do método atual.
As estruturas blittable são muito mais eficientes, pois podem simplesmente ser usadas diretamente pela camada de empacotamento. Tente tornar as estruturas blittable (por exemplo, evite bool
). Para obter mais informações, consulte a seção Tipos de Blittable.
Se a estrutura for blittable, use sizeof()
em vez de Marshal.SizeOf<MyStruct>()
para um melhor desempenho. Como mencionado acima, você pode validar que o tipo é blittable tentando criar um fixo GCHandle
. Se o tipo não for uma string ou considerado blittable, GCHandle.Alloc
lançará um ArgumentException
arquivo .
Ponteiros para estruturas em definições devem ser passados por ref
ou usar unsafe
e *
.
✔️ FAÇA a correspondência entre a estrutura gerenciada o mais próximo possível da forma e dos nomes usados na documentação ou no cabeçalho oficial da plataforma.
✔️ USE o C# sizeof()
em vez de Marshal.SizeOf<MyStruct>()
estruturas blittable para melhorar o desempenho.
❌ EVITE usar classes para expressar tipos nativos complexos por meio de herança.
❌ EVITE usar System.Delegate
campos ou System.MulticastDelegate
para representar campos de ponteiro de função em estruturas.
System.MulticastDelegate Como System.Delegate não têm uma assinatura necessária, eles não garantem que o delegado passado corresponda à assinatura que o código nativo espera. Além disso, no .NET Framework e no .NET Core, empacotar uma struct contendo uma System.Delegate
ou System.MulticastDelegate
de sua representação nativa para um objeto gerenciado pode desestabilizar o tempo de execução se o valor do campo na representação nativa não for um ponteiro de função que encapsula um delegado gerenciado. No .NET 5 e versões posteriores, não há suporte para empacotar um System.Delegate
campo ou System.MulticastDelegate
de uma representação nativa para um objeto gerenciado. Use um tipo de delegado específico em vez de System.Delegate
ou System.MulticastDelegate
.
Buffers fixos
Uma matriz como INT_PTR Reserved1[2]
tem que ser agrupada em dois IntPtr
campos, Reserved1a
e Reserved1b
. Quando a matriz nativa é um tipo primitivo, podemos usar a fixed
palavra-chave para escrevê-la um pouco mais limpamente. Por exemplo, SYSTEM_PROCESS_INFORMATION
tem esta aparência no cabeçalho nativo:
typedef struct _SYSTEM_PROCESS_INFORMATION {
ULONG NextEntryOffset;
ULONG NumberOfThreads;
BYTE Reserved1[48];
UNICODE_STRING ImageName;
...
} SYSTEM_PROCESS_INFORMATION
Em C#, podemos escrevê-lo assim:
internal unsafe struct SYSTEM_PROCESS_INFORMATION
{
internal uint NextEntryOffset;
internal uint NumberOfThreads;
private fixed byte Reserved1[48];
internal Interop.UNICODE_STRING ImageName;
...
}
No entanto, existem alguns gotchas com buffers fixos. Os buffers fixos de tipos não blittable não serão empacotados corretamente, portanto, a matriz in-loco precisa ser expandida para vários campos individuais. Além disso, no .NET Framework e no .NET Core antes da versão 3.0, se uma struct contendo um campo de buffer fixo estiver aninhada em uma struct não blittable, o campo de buffer fixo não será empacotado corretamente para código nativo.