Partilhar via


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.
  • ✔️ 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 for unsigned 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.
  • ✔️ 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 LibraryImportAttributeo . 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:

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

  1. Crie um StringBuilder da capacidade desejada (aloca a capacidade gerenciada) {1}.
  2. Invoque:
    1. Aloca um buffer {2}nativo .
    2. Copia o conteúdo if [In] (o padrão para um StringBuilder parâmetro).
    3. Copia o buffer nativo em uma matriz gerenciada recém-alocada se [Out] {3} (também o padrão para StringBuilder).
  3. 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 ou SysStringFree para cadeias de caracteres marcadas como UnmanagedType.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 _Booltipos 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, uintint, 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.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 exemplo
    • layout fixo requer [StructLayout(LayoutKind.Sequential)] ou [StructLayout(LayoutKind.Explicit)]
    • As 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, 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:

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

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 de dados do Windows

Intervalos de tipos de dados

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

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.