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 class Program
{
    // Import user32.dll (containing the function we need) and define
    // the method corresponding to the native function.
    [DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    private static extern 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 instrução using para o System.Runtime.InteropServices namespace que contém todos os itens necessários.
  • A linha #8 introduz o DllImport atributo. Esse atributo informa ao tempo de execução que ele deve carregar a DLL não gerenciada. A string passada é a DLL em que nossa função de destino está. Além disso, especifica qual conjunto de caracteres usar 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.GetLastWin32Error().
  • 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 tem uma nova palavra-chave que você pode notar, extern, que informa ao tempo de execução que este é um método externo, e que quando você invocá-lo, o tempo de execução deve encontrá-lo na DLL especificada no DllImport atributo.

O resto do exemplo é apenas invocar o método como você faria com qualquer outro método gerenciado.

O exemplo é semelhante para macOS. O nome da biblioteca no atributo precisa mudar, pois o DllImport 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 class Program
    {
        // Import the libSystem shared library and define the method
        // corresponding to the native function.
        [DllImport("libSystem.dylib")]
        private static extern 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 class Program
    {
        // Import the libc shared library and define the method
        // corresponding to the native function.
        [DllImport("libc.so.6")]
        private static extern 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 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.
        [DllImport("user32.dll")]
        private static extern 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 libcbiblioteca 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 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.
        [DllImport("libc.so.6")]
        private static extern 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 DllImport macOS se mantém libc em um lugar diferente.

using System;
using System.Runtime.InteropServices;

namespace PInvokeSamples
{
    public static 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.
        [DllImport("libSystem.dylib")]
        private static extern 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