Вызов неуправляемого кода (P/Invoke)

P/Invoke — это технология, которая позволяет обращаться к структурам, обратным вызовам и функциям в неуправляемых библиотеках из управляемого кода. Большинство API P/Invoke содержится в двух пространствах имен: System и System.Runtime.InteropServices. Эти пространства имен предоставляют средства для описания способа взаимодействия с собственным компонентом.

Начнем с наиболее распространенного примера и вызываем неуправляемые функции в управляемом коде. Давайте покажем окно сообщения из приложения командной строки:

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

Предыдущий пример достаточно прост, но он показывает, что именно нужно для вызова неуправляемых функций из управляемого кода. Рассмотрим пример:

  • Строка 2 показывает инструкцию using для System.Runtime.InteropServices пространства имен, в котором хранятся все необходимые элементы.
  • Строка #8 представляет LibraryImportAttribute атрибут. Этот атрибут сообщает среде выполнения, что она должна загрузить неуправляемый двоичный файл. Строка, передаваемая в, представляет собой неуправляемый двоичный файл, содержащий целевую функцию. Кроме того, он задает кодировку, используемую для маршаллинга строк. В конце строки указано, что эта функция вызывает SetLastError и что среда выполнения должна перехватить этот код ошибки, чтобы пользователь получил его с помощью Marshal.GetLastPInvokeError().
  • Строка #9 — это кромка работы P/Invoke. Она определяет управляемый метод, имеющий точно такую же сигнатуру, что и неуправляемый. Объявление использует LibraryImport атрибут и partial ключевое слово, чтобы сообщить расширению компилятора создать код для вызова неуправляемой библиотеки.
    • В созданном коде и до .NET 7 DllImport используется. Это объявление использует extern ключевое слово, чтобы указать среде выполнения это внешний метод, и что при вызове его среда выполнения должна найти его в неуправляемом двоичном файле, указанном в атрибутеDllImport.

В остальной части примера вызывается метод, как и любой другой управляемый метод.

Для macOS используется аналогичный код. Имя библиотеки в атрибуте LibraryImport необходимо изменить, так как macOS имеет другую схему именования динамических библиотек. В следующем примере используется функция getpid(2), чтобы получить идентификатор процесса приложения и вывести его на консоль.

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

В Linux это осуществляется аналогичным образом. Имя функции такое же, так как getpid(2) является стандартным системным вызовом POSIX.

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

Вызов управляемого кода из неуправляемого кода

Среда выполнения обеспечивает взаимодействие в обоих направлениях, что позволяет вызывать собственные функции обратно в управляемый код с помощью указателей функций. В управляемом коде к указателю функции ближе всего делегат, так как он позволяет выполнять обратные вызовы из машинного кода в управляемом коде.

Этот компонент используется по аналогии с описанным ранее собственным процессом. Для заданного обратного вызова определяется делегат с такой же сигнатурой, который передается во внешний метод. Обо всем остальном позаботится среда выполнения.

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

Перед рассмотрением примера следует просмотреть сигнатуры неуправляемых функций, с которыми нам предстоит работать. Функция, которую требуется вызвать для перечисления всех окон, имеет такую сигнатуру: BOOL EnumWindows (WNDENUMPROC lpEnumFunc, LPARAM lParam);

Первый параметр — это обратный вызов. Он имеет следующую сигнатуру: BOOL CALLBACK EnumWindowsProc (HWND hwnd, LPARAM lParam);

Теперь рассмотрим пример:

  • Строка 9 определяет делегат, который соответствует сигнатуре обратного вызова из неуправляемого кода. Обратите внимание, как типы LPARAM и HWND представлены с помощью IntPtr в управляемом коде.
  • Строки 13 и 14 вводят функцию EnumWindows из библиотеки user32.dll.
  • Строки 17—20 реализуют делегат. В этом простом примере мы просто выводим маркер на консоль.
  • Наконец, в строке 24 вызывается внешний метод и передается делегат.

Ниже приведены примеры для Linux и macOS. В них мы используем функцию ftw, которую можно найти в библиотеке C libc. Эта функция используется для обхода иерархий каталогов и принимает указатель функции в качестве одного из своих параметров. Она имеет следующую сигнатуру: 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;
    }
}

В примере для macOS используется та же функция. Единственное отличие состоит в аргументе для атрибута LibraryImport, так как macOS сохраняет libc в другом месте.

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;
    }
}

Оба предыдущих примера зависят от параметров, и в обоих случаях параметры задаются в виде управляемых типов. На другой стороне среда выполнения преобразует их в эквивалентные. См. дополнительные сведения о маршалировании типов в машинный код.

Дополнительные ресурсы