Invocação de plataforma (P/Invoke)
P/Invoke é uma tecnologia que permite acessar structs, retornos de chamada e funções em bibliotecas não gerenciadas a partir do seu código gerenciado. A maior parte da API P/Invoke está contida em dois namespaces: System
e System.Runtime.InteropServices
. O uso desses dois namespaces fornece as ferramentas para descrever como você deseja se comunicar com o componente nativo.
Vamos começar com o exemplo mais comum, que é chamar funções não gerenciadas em seu código gerenciado. Vamos mostrar uma caixa de mensagem de um aplicativo de linha de comando:
using System;
using System.Runtime.InteropServices;
public partial class Program
{
// Import user32.dll (containing the function we need) and define
// the method corresponding to the native function.
[LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16, SetLastError = true)]
private static partial int MessageBox(IntPtr hWnd, string lpText, string lpCaption, uint uType);
public static void Main(string[] args)
{
// Invoke the function as a regular managed method.
MessageBox(IntPtr.Zero, "Command-line message box", "Attention!", 0);
}
}
O exemplo anterior é simples, mas mostra o que é necessário para invocar funções não gerenciadas do código gerenciado. Vamos ao exemplo:
- A linha #2 mostra a
using
diretiva para oSystem.Runtime.InteropServices
namespace que contém todos os itens necessários. - A linha #8 introduz o LibraryImportAttribute atributo. Esse atributo informa ao tempo de execução que ele deve carregar o binário não gerenciado. A cadeia de caracteres passada é o binário não gerenciado que contém a função de destino. Além disso, especifica a codificação a ser usada para organizar as cadeias de caracteres. Finalmente, ele especifica que essa função chama SetLastError e que o tempo de execução deve capturar esse código de erro para que o usuário possa recuperá-lo via Marshal.GetLastPInvokeError().
- A linha #9 é o ponto crucial do trabalho P/Invoke. Ele define um método gerenciado que tem exatamente a mesma assinatura que o método não gerenciado. A declaração usa o
LibraryImport
atributo e apartial
palavra-chave para dizer a uma extensão do compilador para gerar código para chamar para a biblioteca não gerenciada.- Dentro do código gerado e antes do .NET 7, o
DllImport
é usado. Essa declaração usa aextern
palavra-chave para indicar ao tempo de execução que este é um método externo e que, quando você o invoca, o tempo de execução deve encontrá-lo no binário não gerenciado especificado noDllImport
atributo.
- Dentro do código gerado e antes do .NET 7, o
O resto do exemplo é invocar o método como faria com qualquer outro método gerenciado.
O exemplo é semelhante para macOS. O nome da biblioteca no atributo precisa mudar, pois o LibraryImport
macOS tem um esquema diferente de nomear bibliotecas dinâmicas. O exemplo a seguir usa a getpid(2)
função para obter a ID do processo do aplicativo e imprimi-la no console:
using System;
using System.Runtime.InteropServices;
namespace PInvokeSamples
{
public static partial class Program
{
// Import the libSystem shared library and define the method
// corresponding to the native function.
[LibraryImport("libSystem.dylib")]
private static partial int getpid();
public static void Main(string[] args)
{
// Invoke the function and get the process ID.
int pid = getpid();
Console.WriteLine(pid);
}
}
}
Também é semelhante no Linux. O nome da função é o mesmo, uma vez que getpid(2)
é uma chamada de sistema POSIX padrão.
using System;
using System.Runtime.InteropServices;
namespace PInvokeSamples
{
public static partial class Program
{
// Import the libc shared library and define the method
// corresponding to the native function.
[LibraryImport("libc.so.6")]
private static partial int getpid();
public static void Main(string[] args)
{
// Invoke the function and get the process ID.
int pid = getpid();
Console.WriteLine(pid);
}
}
}
Invocando código gerenciado de código não gerenciado
O tempo de execução permite que a comunicação flua em ambas as direções, permitindo que você chame de volta para o código gerenciado a partir de funções nativas usando ponteiros de função. A coisa mais próxima de um ponteiro de função no código gerenciado é um delegado, então isso é o que é usado para permitir retornos de chamada do código nativo para o código gerenciado.
A maneira de usar esse recurso é semelhante ao processo gerenciado para nativo descrito anteriormente. Para um determinado retorno de chamada, você define um delegado que corresponde à assinatura e passa isso para o método externo. O tempo de execução cuidará de todo o resto.
using System;
using System.Runtime.InteropServices;
namespace ConsoleApplication1
{
public static partial class Program
{
// Define a delegate that corresponds to the unmanaged function.
private delegate bool EnumWC(IntPtr hwnd, IntPtr lParam);
// Import user32.dll (containing the function we need) and define
// the method corresponding to the native function.
[LibraryImport("user32.dll")]
private static partial int EnumWindows(EnumWC lpEnumFunc, IntPtr lParam);
// Define the implementation of the delegate; here, we simply output the window handle.
private static bool OutputWindow(IntPtr hwnd, IntPtr lParam)
{
Console.WriteLine(hwnd.ToInt64());
return true;
}
public static void Main(string[] args)
{
// Invoke the method; note the delegate as a first parameter.
EnumWindows(OutputWindow, IntPtr.Zero);
}
}
}
Antes de passar pelo exemplo, é bom revisar as assinaturas das funções não gerenciadas com as quais você precisa trabalhar. A função a ser chamada para enumerar todas as janelas tem a seguinte assinatura: BOOL EnumWindows (WNDENUMPROC lpEnumFunc, LPARAM lParam);
O primeiro parâmetro é um retorno de chamada. O referido retorno de chamada tem a seguinte assinatura: BOOL CALLBACK EnumWindowsProc (HWND hwnd, LPARAM lParam);
Agora, vamos ao exemplo:
- A linha #9 no exemplo define um delegado que corresponde à assinatura do retorno de chamada do código não gerenciado. Observe como os tipos LPARAM e HWND são representados usando
IntPtr
o código gerenciado. - As linhas #13 e #14 introduzem a
EnumWindows
função a partir da biblioteca user32.dll. - As linhas #17 - 20 implementam o delegado. Para este exemplo simples, queremos apenas exportar o identificador para o console.
- Finalmente, na linha #24, o método externo é chamado e passado no delegado.
Os exemplos de Linux e macOS são mostrados abaixo. Para eles, usamos a ftw
função que pode ser encontrada na libc
biblioteca C. Esta função é usada para percorrer hierarquias de diretório e leva um ponteiro para uma função como um de seus parâmetros. A referida função tem a seguinte assinatura: int (*fn) (const char *fpath, const struct stat *sb, int typeflag)
.
using System;
using System.Runtime.InteropServices;
namespace PInvokeSamples
{
public static partial class Program
{
// Define a delegate that has the same signature as the native function.
private delegate int DirClbk(string fName, ref Stat stat, int typeFlag);
// Import the libc and define the method to represent the native function.
[LibraryImport("libc.so.6", StringMarshalling = StringMarshalling.Utf16)]
private static partial int ftw(string dirpath, DirClbk cl, int descriptors);
// Implement the above DirClbk delegate;
// this one just prints out the filename that is passed to it.
private static int DisplayEntry(string fName, ref Stat stat, int typeFlag)
{
Console.WriteLine(fName);
return 0;
}
public static void Main(string[] args)
{
// Call the native function.
// Note the second parameter which represents the delegate (callback).
ftw(".", DisplayEntry, 10);
}
}
// The native callback takes a pointer to a struct. This type
// represents that struct in managed code.
[StructLayout(LayoutKind.Sequential)]
public struct Stat
{
public uint DeviceID;
public uint InodeNumber;
public uint Mode;
public uint HardLinks;
public uint UserID;
public uint GroupID;
public uint SpecialDeviceID;
public ulong Size;
public ulong BlockSize;
public uint Blocks;
public long TimeLastAccess;
public long TimeLastModification;
public long TimeLastStatusChange;
}
}
O exemplo do macOS usa a mesma função, e a única diferença é o argumento para o atributo, já que o LibraryImport
macOS se mantém libc
em um lugar diferente.
using System;
using System.Runtime.InteropServices;
namespace PInvokeSamples
{
public static partial class Program
{
// Define a delegate that has the same signature as the native function.
private delegate int DirClbk(string fName, ref Stat stat, int typeFlag);
// Import the libc and define the method to represent the native function.
[LibraryImport("libSystem.dylib", StringMarshalling = StringMarshalling.Utf16)]
private static partial int ftw(string dirpath, DirClbk cl, int descriptors);
// Implement the above DirClbk delegate;
// this one just prints out the filename that is passed to it.
private static int DisplayEntry(string fName, ref Stat stat, int typeFlag)
{
Console.WriteLine(fName);
return 0;
}
public static void Main(string[] args)
{
// Call the native function.
// Note the second parameter which represents the delegate (callback).
ftw(".", DisplayEntry, 10);
}
}
// The native callback takes a pointer to a struct. This type
// represents that struct in managed code.
[StructLayout(LayoutKind.Sequential)]
public struct Stat
{
public uint DeviceID;
public uint InodeNumber;
public uint Mode;
public uint HardLinks;
public uint UserID;
public uint GroupID;
public uint SpecialDeviceID;
public ulong Size;
public ulong BlockSize;
public uint Blocks;
public long TimeLastAccess;
public long TimeLastModification;
public long TimeLastStatusChange;
}
}
Ambos os exemplos anteriores dependem de parâmetros e, em ambos os casos, os parâmetros são dados como tipos gerenciados. O tempo de execução faz a "coisa certa" e processa-os em seus equivalentes do outro lado. Saiba mais sobre como os tipos são empacotados para código nativo em nossa página sobre Empacotamento de tipos.
Mais recursos
- Escrevendo P/Invokes entre plataformas
- Agrupamento P/Invoke gerado na origem
- O gerador de código-fonte C#/Win32 P/Invoke gera automaticamente definições para APIs do Windows.
- P/Invoke em C++/CLI
- Documentação mono em P/Invoke