Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
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.
- ✔️ DO define P/Invoke e assinaturas de ponteiro de função que correspondem aos argumentos da função C.
- ✔️ USE tipos .NET que mapeiam mais próximos do tipo nativo. Por exemplo, em C#, use
uintquando 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
Delegatetipos, 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#nameofpara 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
SafeHandleidentificadores 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, ver Implementar um método Dispose.
Configurações do atributo LibraryImport
Um analisador de código, com ID SYSLIB1054, ajuda a orientá-lo com LibraryImportAttribute. Na maioria dos casos, o uso de LibraryImportAttribute requer uma declaração explícita em vez de depender de 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
| Configuraçã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 esta opção como verdadeira para obter um pequeno benefício de desempenho, pois durante a execução, não será necessário procurar nomes alternativos de funções com um sufixo "A" ou "W", dependendo do valor da configuração CharSet ("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 por valor com o atributo [Out] podem desestabilizar o tempo de execução se a cadeia de caracteres for uma cadeia de caracteres internada. Veja mais informação sobre a internação de strings na documentação do String.Intern.
✔️ CONSIDERE arrays de um char[] ou byte[] quando se espera que o código nativo preencha um ArrayPool buffer de caracteres. Isso requer passar o argumento como [Out].
Orientação específica para 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
StringBuilderda capacidade desejada (aloca a capacidade gerenciada) {1}. - Invoque:
- Aloca um buffer {2}nativo .
- Copia o conteúdo if
[In](o padrão para umStringBuilderparâmetro). - Copia o buffer nativo numa matriz gerida recém-alocada se
[Out]{3}(também é o padrão paraStringBuilder).
-
ToString()aloca mais uma matriz {4}gerenciada.
São estas {4} alocações para obter uma string 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 ir diretamente à alocação específica para o ToString() em chamadas subsequentes.
O outro problema com StringBuilder é que ele sempre copia o buffer de retorno até o primeiro nulo. Se a cadeia de caracteres passada não tiver terminação ou for uma cadeia de caracteres com terminação dupla nula, a sua invocação P/Invoke está, no melhor dos casos, incorreta.
Se você usarStringBuilder, um último problema é que a capacidade não inclui um nulo oculto, que é sempre contabilizado na interoperabilidade. É comum que as pessoas errarem, 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 Personalização do empacotamento de strings.
Específico do Windows Para
[Out]cadeias de caracteres, o CLR usaráCoTaskMemFreepor padrão para liberar cadeias de caracteres ouSysStringFreepara 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 confundir. Por padrão, um .NET bool é empacotado para um Windows BOOL, onde é um valor de 4 bytes. No entanto, os tipos _Bool e bool 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.
Identificadores Globais Únicos (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 geridos eficientemente entre o código nativo, e como isso melhora o desempenho, deveriam ser a escolha preferida. Certos tipos não são diretamente convertíveis, mas são conhecidos por conter conteúdos que podem ser convertidos diretamente. 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 estão em campos de structs ou para fins de UnmanagedCallersOnlyAttribute.
Tipos blittable quando o processamento de runtime está habilitado
Tipos litíveis:
-
byte,sbyte, ,short,ushort,intuint,long,ulongsingle,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.Sequentialpor padrão
- layout fixo requer
Tipos com conteúdo blittable:
- matrizes não aninhadas e unidimensionais 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 por vezes blittable:
string
Quando tipos blittable são passados por referência com in, refou , ou outquando 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 verificar se um tipo é blittable ou contém conteúdos blittable tentando criar um objeto fixado GCHandle. Se o tipo não for uma string ou considerado blittable, GCHandle.Alloc lançará uma ArgumentException.
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. Pode ser usado em vez de IntPtr em assinaturas de 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 consiste em passar uma referência de um objeto gerido através de código nativo e retornar ao código gerido, geralmente com um callback. 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.
| Largura | Mac OS | 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 IntPtrUIntPtr, mas prefere void* quando possível.
Tipos suportados anteriormente incorporados
Há casos raros em que o suporte integrado para um tipo é removido.
O suporte interno de UnmanagedType.HString e UnmanagedType.IInspectable para marshal foi 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 |
|---|---|---|
| Mac OS | 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 almejar o .NET 5 e versões anteriores, deve-se declarar assinaturas separadas para Windows e não-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 obter o endereço em blocos de código inseguro 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 em vez de sizeof() para um melhor desempenho. Como mencionado acima, pode validar que o tipo é blittable tentando criar um objeto fixado GCHandle. Se o tipo não for uma string ou considerado blittable, GCHandle.Alloc irá lançar um ArgumentException.
Ponteiros para estruturas em definições devem ser passados por ref ou usar unsafe e *.
✔️ FAÇA a correspondência entre a estrutura gerida o mais próximo possível da forma e dos nomes utilizados na documentação ou nos cabeçalhos oficiais da plataforma.
✔️ UTILIZE o C# sizeof() em vez de Marshal.SizeOf<MyStruct>() para estruturas blittable para melhorar o desempenho.
❌ NÃO dependa da representação interna de tipos struct expostos por bibliotecas do ambiente de execução do .NET, a menos que esteja explicitamente documentada.
❌ EVITE usar classes para representar tipos complexos nativos através de herança.
❌ EVITE usar campos System.Delegate ou System.MulticastDelegate para representar ponteiros de função em estruturas.
Como System.Delegate e System.MulticastDelegate não têm uma assinatura obrigatória, eles não garantem que a delegação passada corresponda à assinatura que o código nativo requer. 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. Nas versões .NET 5 e posteriores, o marshaling de um campo System.Delegate ou System.MulticastDelegate de uma representação nativa para um objeto gerenciado não é suportado. 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 algumas armadilhas relacionadas a buffers fixos. Os buffers fixos de tipos não blittable não serão empacotados corretamente; por isso, a matriz no local precisa ser expandida para múltiplos 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 dentro de uma struct não blittable, o campo de buffer fixo não será corretamente convertido para código nativo.