Práticas recomendadas de interoperabilidade nativa

O .NET oferece várias maneiras de personalizar seu código de interoperabilidade nativa. Este artigo inclui as diretrizes que as equipes .NET da Microsoft seguem para a interoperabilidade nativa.

Orientação geral

As diretrizes nesta seção se aplicam a todos os cenários de interoperabilidade.

  • ✔️ USE a mesma nomenclatura e uso de maiúsculas para seus métodos e parâmetros como o método nativo que você deseja chamar.
  • ✔️ CONSIDERE usar a mesma nomenclatura e uso de maiúsculas para valores constantes.
  • ✔️ USE tipos .NET com mapeamento mais próximo do tipo nativo. Por exemplo, no caso de C#, use uint quando o tipo nativo for unsigned int.
  • ✔️ PREFIRA expressar tipos nativos de nível mais alto usando estruturas em .NET em vez de classes.
  • ✔️ USE os atributos [In] e [Out] em parâmetros de matriz.
  • ✔️ USE os atributos [In] e [Out] em outros tipos quando o comportamento desejado difere do comportamento padrão.
  • ✔️ CONSIDERE usar System.Buffers.ArrayPool<T> para agrupar seus buffers de matriz nativos.
  • ✔️ CONSIDERE encapsular suas declarações P/Invoke em uma classe com o mesmo nome e letras maiúsculas como sua biblioteca nativa.
    • Isso permite que seus atributos [DllImport] usem o recurso de linguagem C# nameof para passar o nome da biblioteca nativa e garantir que você não tenha digitado errado o nome da biblioteca nativa.

Configurações de atributo DllImport

Configuração Padrão Recomendação Detalhes
PreserveSig true mantenha o padrão Quando esta configuração é definida como false, valores de retorno HRESULT com falha serão considerados exceções (e o valor de retorno na definição torna-se nulo).
SetLastError false Depende da API Defina esta configuração como true se a API usa GetLastError e usa Marshal.GetLastWin32Error para obter o valor. Se a API definir uma condição que informa um erro, obtenha o erro antes de fazer outras chamadas para evitar que ele seja sobrescrito inadvertidamente.
CharSet Definido pelo compilador (especificado na documentação do conjunto de caracteres) Use explicitamente CharSet.Unicode ou CharSet.Ansi quando os caracteres ou cadeias de caracteres estiverem presentes na definição Isso especifica o comportamento de marshalling de cadeias de caracteres e o que ExactSpelling faz quando false. Note que CharSet.Ansi é na verdade UTF8 no Unix. O Windows usa Unicode a maior parte do tempo, enquanto o Unix usa UTF8. Veja mais informações na documentação sobre conjuntos de caracteres.
ExactSpelling false true Defina como true e obtenha um pequeno benefício de desempenho: o runtime não irá buscar por nomes de função alternativos com o 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

Quando o CharSet é Unicode ou o argumento é explicitamente marcado como [MarshalAs(UnmanagedType.LPWSTR)]e a cadeia de caracteres é passada por valor (não ref ou out), a cadeia de caracteres será fixada e usada diretamente pelo código nativo (em vez de copiado).

❌ NÃO use parâmetros [Out] string. Os parâmetros de cadeia de caracteres passados por valor com o atributo [Out] podem desestabilizar o runtime se a cadeia de caracteres for uma cadeia de caracteres internada. Veja mais informações sobre a centralização da cadeia de caracteres na documentação do String.Intern.

✔️ CONSIDERE definir a propriedade CharSet em [DllImport] para que o runtime conheça a codificação de cadeia de caracteres esperada.

✔️ CONSIDERE matrizes char[] ou byte[] de um ArrayPool quando for esperado que o código nativo preencha um buffer de caracteres. Isso requer passar o argumento como [Out].

✔️ CONSIDERE evitar parâmetros StringBuilder. O marshalling de StringBuildersempre cria uma cópia do buffer nativo. Dessa forma, ele pode ser extremamente ineficiente. Veja o cenário típico da chamada de uma API do Windows que usa uma cadeia de caracteres:

  1. Criar um StringBuilder da capacidade desejada (aloca capacidade gerenciada) {1}.
  2. Invoque:
    1. Aloca um buffer nativo {2}.
    2. Copia o conteúdo se [In](o padrão para um parâmetro StringBuilder).
    3. Copia o buffer nativo em uma matriz gerenciada que acaba de ser alocada se [Out]{3}(também é o padrão para StringBuilder).
  3. ToString() aloca outra matriz gerenciada {4}.

Isso são {4} alocações para obter uma cadeia de caracteres fora do código nativo. O melhor que você pode fazer para limitar isso é reutilizar o StringBuilder em outra chamada, mas isso economiza apenas um alocação. É muito melhor usar e armazenar em cache um buffer de caractere de ArrayPool. Você pode então reduzir para apenas a alocação para ToString() nas chamadas subsequentes.

O outro problema com StringBuilder é que esta configuração sempre copia o buffer de retorno de volta para o primeiro nulo. Se a cadeia de caracteres transmitida não estiver terminada, ou terminar por dois caracteres nulos, na melhor das hipóteses, o recurso P/Invoke estará incorreto.

Se você usar o StringBuilder, uma última pegadinha é que a capacidade não inclui um nulo oculto, que é sempre contabilizado na interoperabilidade. É comum as pessoas entenderem errado, já que a maioria das APIs deseja o tamanho do buffer, incluindo o valor nulo. Isso pode resultar em alocações desnecessárias/desperdiçadas. Além disso, essa pegadinha evita que o runtime otimize o marshalling de StringBuilder para minimizar as cópias.

Para saber mais sobre o marshalling de cadeia de caracteres, confira Marshalling padrão para cadeia de caracteres e Personalizar o marshalling de cadeia de caracteres.

Específico do Windows Para cadeias de caracteres [Out], a CLR usará CoTaskMemFree por padrão para liberar cadeias de caracteres, ou SysStringFree para cadeias de caracteres que são marcadas como UnmanagedType.BSTR. Para a maioria das APIs com um buffer de cadeia de caracteres de saída: a contagem de caracteres passada deve incluir o nulo. Se o valor retornado for menor que a contagem de caracteres transmitidos, a chamada foi bem-sucedida e o valor consiste no número de caracteres sem o nulo à direita. Caso contrário, a contagem consiste no tamanho necessário do buffer incluindo o caractere nulo.

  • Passe cinco, obtenha quatro: a cadeia de caracteres tem quatro caracteres com um nulo à direita.
  • Passar cinco, obter seis: a cadeia de caracteres tem cinco caracteres, precisa de um buffer de seis caracteres para manter o valor nulo. Tipos de dados do Windows para cadeias de caracteres

Parâmetros e campos boolianos

É fácil cometer erros com boolianos. Por padrão, um bool.NET realiza marshalling para um BOOL Windows, onde é um valor de 4 bytes. No entanto, os tipos _Bool e bool em C e C++ são um byte único. Isso pode dificultar o rastreamento de bugs, já que metade do valor de retorno será descartado, o que só potencialmente alterará o resultado. Para saber mais sobre como realizar marshalling de valores .NET bool para os tipos C ou C ++ bool, consulte a documentação sobre como personalizar o marshalling de campos boolianos.

GUIDs

Os GUIDs podem ser usados diretamente em assinaturas. Muitas APIs do Windows usam aliases do tipo GUID& como REFIID. Quando a assinatura do método contiver um parâmetro de referência, coloque uma palavra-chave ref ou um atributo [MarshalAs(UnmanagedType.LPStruct)] na declaração de parâmetro GUID.

GUID GUID by-ref
KNOWNFOLDERID REFKNOWNFOLDERID

❌ NÃO use [MarshalAs(UnmanagedType.LPStruct)] para nada além de parâmetros GUID ref.

Tipos blittable

Os tipos blittable são tipos que têm a mesma representação em nível de bits no código gerenciado e nativo. Dessa forma, eles não precisam ser convertidos em outro formato para serem organizados de e para código nativo, e como isso melhora o desempenho, eles devem ter a preferência. Alguns tipos não são blittable, mas é sabido que eles apresentam 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 marshalling de runtime está habilitado

Tipos blittable:

  • byte, sbyte, short, ushort, int, uint, long, ulong, single, double
  • structs com layout fixo que só têm tipos de valor blittable para campos de instância
    • layout fixo requer [StructLayout(LayoutKind.Sequential)] ou [StructLayout(LayoutKind.Explicit)]
    • structs são LayoutKind.Sequential por padrão

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 instância
    • layout fixo requer [StructLayout(LayoutKind.Sequential)] ou [StructLayout(LayoutKind.Explicit)]
    • classes são LayoutKind.Auto por padrão

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 out, ou 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 é explicitamente marcado com [StructLayout] com CharSet = CharSet.Unicode.

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

string apresenta conteúdo blittable se não estiver em outro tipo e estiver sendo passado como um argumento que é marcado com [MarshalAs(UnmanagedType.LPWStr)] ou o [DllImport] tiver CharSet = CharSet.Unicode definido.

Você pode ver se um tipo é blittable ou tem conteúdos blittable tentando criar um GCHandle fixado. Se o tipo não for uma cadeia de caracteres ou considerado blittable, GCHandle.Alloc lançará um ArgumentException.

Tipos Blittable quando o marshalling de runtime está desabilitado

Quando o marshalling de runtime é desabilitado, 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 cadeias de caracteres, não se aplica quando o marshalling de runtime está desabilitado. Qualquer tipo que não seja considerado blittable pela regra mencionada acima não tem suporte quando o marshalling de runtime está desabilitado.

Essas regras diferem do sistema interno principalmente em situações em que bool e char são usados. Quando o marshalling está desabilitado, bool é passado como um valor de 1 byte e não é normalizado e char sempre é passado como um valor de 2 bytes. Quando o marshalling de runtime 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.

✔️ TORNE suas estruturas mais blittable quando possível.

Para obter mais informações, consulte:

Manter objetos gerenciados ativos

GC.KeepAlive() garantirá que um objeto permaneça no escopo até que o método KeepAlive seja alcançado.

HandleRef permite ao marshaller manter um objeto ativo pela duração de um P/Invoke. Ele pode ser usado em vez de IntPtr em assinaturas de métodos. SafeHandle substitui efetivamente essa classe e deve ser usado em seu lugar.

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();

Fixar não é o padrão para GCHandle. O outro padrão principal é passar uma referência a um objeto gerenciado por meio do código nativo e voltar ao 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 perda de memória.

Tipos de dados comuns do Windows

Veja a seguir uma lista dos tipos de dados comumente usados em APIs do Windows e quais tipos C# devem ser usados ao chamar o código do Windows.

Os tipos a seguir são do mesmo tamanho no Windows de 32 e 64 bits, apesar de seus 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 Confira CLong e CULong.
32 LONG32 int
32 CLONG uint Confira CLong e CULong.
32 DWORD uint Confira CLong e CULong.
32 DWORD32 uint
32 UINT uint
32 UINT32 uint
32 ULONG uint Confira 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 tipos a seguir, sendo ponteiros, seguem a largura da plataforma. Use IntPtr/UIntPtr para eles.

Tipos de ponteiros assinados (use IntPtr) Tipos de ponteiros 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 passar por marshalling como IntPtr ou UIntPtr, mas prefere void* quando possível.

Tipos de dados do Windows

Intervalos de tipos de dados

Tipos anteriormente com suporte interno

Há casos raros em que o suporte interno para um tipo é removido.

O suporte interno do marshal UnmanagedType.HString e UnmanagedType.IInspectable foi removido na versão do .NET 5. Você deve recompilar binários que usam esse tipo de marshalling e que direcionam uma estrutura anterior. Ainda é possível realizar marshaling desse tipo, mas você deve fazer isso manualmente, como mostra o exemplo de código a seguir. Esse 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 tipo de dados multiplataforma

Há tipos na linguagem C/C++ que têm latitude na forma como são definidos. Ao gravar interoperabilidade multiplataforma, podem surgir casos em que as plataformas diferem e podem causar problemas se não forem considerados.

C/C++ long

C/C++ e longC#long não são necessariamente do mesmo tamanho.

O tipo long em C/C++ é definido como com "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

Por outro lado, 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 o C/C++ long não existe para C/C++ char, short, int e long long, pois são de 8, 16, 32 e 64 bits, respectivamente, em todas essas plataformas.

No .NET 6 e versões posteriores, use os tipos CLong e CULong para interoperabilidade com os tipos de dados C/C++ long e unsigned long. 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 ter como alvo 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 structs gerenciadas são criadas e não são removidas até o método retornar. Por definição, elas são "fixadas" (não serão movidas 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 têm um melhor desempenho, pois podem simplesmente ser usadas diretamente pela camada de marshalling. Tente tornar structs blittable (por exemplo, evite bool). Para saber mais, veja a seção Tipos blittable.

Se a struct é blittable, use sizeof() em vez de Marshal.SizeOf<MyStruct>() para melhor desempenho. Como mencionado acima, você pode validar que o tipo é blittable ao tentar criar um GCHandle fixado. Se o tipo não for uma cadeia de caracteres ou considerado blittable, GCHandle.Alloc lançara ArgumentException.

Os ponteiros para structs nas definições devem ser transmitidos por ref ou usar unsafe e *.

✔️ BUSQUE a struct gerenciada o mais próximo possível da forma e dos nomes usados na documentação ou no cabeçalho da plataforma oficial.

✔️ USEsizeof() C# em vez de Marshal.SizeOf<MyStruct>() para estruturas blittable a fim de melhorar o desempenho.

❌ EVITE usar classes para expressar tipos nativos complexos por meio de herança.

❌ EVITE usar campos System.Delegate ou System.MulticastDelegate para representar campos de ponteiro de função em estruturas.

Como System.Delegate e System.MulticastDelegate não têm uma assinatura necessária, eles não garantem que o delegado passado corresponda à assinatura esperada pelo código nativo. Além disso, no .NET Framework e no .NET Core, o marshalling de um struct que contém um System.Delegate ou System.MulticastDelegate de sua representação nativa para um objeto gerenciado poderá desestabilizar o runtime 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 o marshalling de um campo System.Delegate 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 corrigidos

Uma matriz como INT_PTR Reserved1[2] precisa ser empacotada para dois campos IntPtr, Reserved1a e Reserved1b. Quando a matriz nativa é um tipo primitivo, podemos usar a palavra-chave fixed para escrevê-la um pouco mais limpa. Por exemplo, SYSTEM_PROCESS_INFORMATION se parece com isso 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 com buffers fixos. Os buffers fixos de tipos não blittable não serão empacotados corretamente, então a matriz in-loco precisa ser expandido para múltiplos campos individuais. Além disso, no .NET Framework e .NET Core antes de 3.0, se uma struct contendo um campo de buffer fixo estiver aninhada em uma struct não blittable, não será realizado um marshalling correto do campo de buffer fixo para o código nativo.