Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
.NET dá-lhe várias formas de personalizar o seu código nativo de interoperabilidade. Este artigo inclui as orientações que as equipas de .NET da Microsoft seguem para 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 para .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.
- ✔️ Utilize tipos .NET que correspondam mais ao tipo nativo. Por exemplo, em C#, use
uintquando o tipo nativo forunsigned int. - ✔️ O DO prefere expressar tipos nativos de nível mais alto usando structs .NET em vez de classes.
- ✔️ Prefira usar ponteiros de função e UnmanagedCallersOnlyAttribute em vez de tipos
Delegateao passar callbacks para funções não gerida em C#. Para obter mais informações, veja GetFunctionPointerForDelegate(Delegate). - ✔️ 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 Windows usa Unicode enquanto 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. Veja-se o cenário típico de chamar uma API do Windows que aceita uma string:
- 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.
Windows Específico Para
[Out]cadeias de caracteres, o CLR usaráCoTaskMemFreepor padrão para libertar 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. Windows Tipos de Dados para Cadeias de Caracteres
Parâmetros e campos booleanos
Booleanos são fáceis de confundir. Por defeito, um .NET bool é marshallado 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 mais informações sobre o marshalling de valores .NET
Identificadores Globais Únicos (GUIDs)
GUIDs são utilizáveis diretamente em assinaturas. Muitas APIs Windows usam alias do tipo GUID& 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 no Windows
Aqui está uma lista dos tipos de dados comumente usados nas APIs do Windows e quais tipos C# usar ao aceder ao código do Windows.
Os seguintes tipos têm o mesmo tamanho no Windows de 32 e 64 bits, apesar dos nomes.
| Largura | 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 marshallado como IntPtr ou UIntPtr, mas prefere void* sempre que possível.
Tipos suportados anteriormente incorporados
Há casos raros em que o suporte integrado para um tipo é removido.
O suporte de marshal incorporado UnmanagedType.HString e UnmanagedType.IInspectable 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 |
|---|---|---|
| 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.)
Nas versões .NET 6 e posteriores, utilize-se os tipos CLong 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 compilar para .NET 5 e versões anteriores, deve declarar assinaturas separadas para Windows e não Windows para resolver 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 dependas da representação interna dos tipos de struct expostos pelas bibliotecas 'runtime' do .NET, a menos que esteja explicitamente documentado.
❌ 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 .NET Core, o marshalling de uma struct contendo um System.Delegate ou System.MulticastDelegate da sua representação nativa para um objeto gerido pode desestabilizar o tempo de execução caso o valor do campo na representação nativa não seja um ponteiro de função que encapsula um delegado gerido. Nas versões .NET 5 e posteriores, o marshalling de um campo System.Delegate ou System.MulticastDelegate de uma representação nativa para um objeto gerido 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 3.0, se uma struct contendo um campo de buffer fixo estiver aninhada dentro de uma struct não blitável, o campo de buffer fixo não será corretamente marshallado para código nativo.
Solução de falhas de P/Invoke
A tabela seguinte relaciona os sintomas comuns com a sua causa provável e a solução recomendada.
| Symptom | Causa provável | Corrigir |
|---|---|---|
| DllNotFoundException | Biblioteca não encontrada em tempo de execução | Verifique o nome da biblioteca, o percurso e a plataforma. Uso TryLoad para testar o carregamento. No Linux, verificar LD_LIBRARY_PATH ou rpath. |
| EntryPointNotFoundException | Incompatibilidade de nomes de exportação | Inspecionar exportações nativas (dumpbin /exports no Windows, nm -D no Linux). Verifique a transformação dos nomes em C++ (falta extern "C"). Defina EntryPoint explicitamente. |
| AccessViolationException | Assinatura incompatível, uso após liberação ou falta de imobilização | Compare assinaturas geridas e nativas. Verifica os tamanhos das estruturas com Marshal.SizeOf<T>() vs nativo sizeof. Verifique a duração da memória. Use uma assinatura blitable para resolver problemas de marshalling |
| Corrupção silenciosa de dados | Tamanho de tipo de letra ou codificação errados | Adicionar registo de atividades nos limites. Compare Marshal.SizeOf<T>() com o nativo sizeof. Teste com pares de entrada/saída conhecidos. |
| Acidentes intermitentes | O GC moveu um objeto sem fixação ou recolheu um delegado | Delegados de root para chamadas de retorno durante toda a sua vida. Uso GCHandle ou fixed para ponteiros mantidos entre chamadas. |
| Corrupção de heap na libertação | Alocador errado | Iguale o alocador: nunca misture malloc/free com CoTaskMemAlloc/CoTaskMemFree ou Marshal.FreeHGlobal. Utilize a função gratuita da própria biblioteca. |
Prevenir a recolha de delegados com GC.KeepAlive
Quando se usa GetFunctionPointerForDelegate para converter um delegado num ponteiro de função, o coletor de lixo não acompanha a relação entre o ponteiro retornado e o delegado de origem. Se o delegado for elegível para recolha antes de o código nativo terminar de usar o ponteiro, a aplicação irá crashar.
Uso KeepAlive para evitar a recolha:
var callback = new MyDelegate((level, msgPtr) =>
{
string msg = Marshal.PtrToStringUTF8(msgPtr) ?? string.Empty;
Console.WriteLine($"[{level}] {msg}");
});
IntPtr fnPtr = Marshal.GetFunctionPointerForDelegate(callback);
NativeUsesCallback(fnPtr);
GC.KeepAlive(callback); // Prevent collection — fnPtr does not root the delegate
Se o código nativo armazenar o ponteiro da função para além da chamada (por exemplo, como um callback persistente), o delegado deve ser enraizado durante toda a sua vida útil — normalmente armazenando-o num static campo.
Resolver conflitos entre documentação e cabeçalhos nativos
Ao escrever assinaturas P/Invoke, é possível encontrar discrepâncias entre a documentação online da API e os ficheiros nativos de cabeçalho. Os ficheiros de cabeçalho são a fonte autoritativa para assinaturas de funções, layouts de estruturas, tamanhos de tipo e convenções de chamada. Em caso de dúvida, verifique as suas assinaturas de P/Invoke em relação ao cabeçalho em vez de confiar apenas na documentação.