Введение

В своей практике я несколько раз сталкивался с задачей интеграции и взаимодействия с низкоуровневыми языками программирования (C/C++) и низкоуровневыми API, такими как Windows API.

Этот туториал упрощает мой опыт использования низкоуровневых языков и API, а также демонстрирует, как написать и интегрировать простую C-библиотеку в ваше C# приложение с интеграцией Windows API.

Эта тема имеет специальное название — Platform Invocation (P/Invoke) в .NET.

PS: Есть и другие простые способы решения этой проблемы. Вы можете воспользоваться любым из них, но этот урок поможет вам понять, как работает P/Invoke.

P/Invoke (Platform Invocation) — это мощный механизм в C#, который позволяет взаимодействовать с неуправляемыми библиотеками кода (как правило, DLL) из управляемых приложений .NET. Это дает возможность использовать существующие кодовые базы на C или C++ или получать доступ к системным функциям, которые напрямую не поддерживаются в .NET.

P/Invoke играет важную роль в C#, так как позволяет преодолеть разрыв между управляемым миром .NET и неуправляемым миром нативного кода (обычно на C или C++).

Многие компании имеют значительные вложения в код на C или C++. P/Invoke позволяет использовать эту функциональность без необходимости переписывать её на C#. Это зачастую более эффективно, чем создавать всё с нуля.

О, я забыл упомянуть: если чтение кажется скучным, вот русская версия видео на YouTube, где я всё объясняю с нуля и более подробно.

И вот английская версия видео. Небольшая заметка: я записываю каждое видео с нуля, это не переведённое видео.

Мы обычно работаем с управляемым кодом в .NET, но иногда нам необходимо интегрировать устройства, у которых нет драйвера/обертки на C#. Большинство таких интеграций написаны на низкоуровневых языках, таких как C/C++.

P/Invoke предоставляет доступ к системному оборудованию и устройствам, которые могут быть не напрямую поддержаны в .NET. Для задач, требующих максимальной производительности, таких как реальное время обработки или высокопроизводительные приложения, прямое взаимодействие с операционной системой может быть полезным.

Некоторые функции доступны только через платформоспецифичные API, которые можно вызвать с помощью P/Invoke. Если требуемая библиотека доступна только в нативной форме, P/Invoke — это путь к её использованию.

Мой первый опыт использования Platform Invocation был в 2013 году, когда мы планировали интеграцию считывателя смарт-карт в наше C# приложение. Драйвер для этого устройства был написан на C.

Вызов Windows API через P/Invoke

В этой части мы сосредоточимся на интеграции функций user32.dll в наше C# приложение.

User32.dll — это важный компонент операционной системы Windows, который отвечает за управление пользовательским интерфейсом (UI). Он предоставляет основные функции для создания, управления и отображения окон, меню, диалоговых окон и других графических элементов.

Основные функции user32.dll включают:

  • Управление окнами: создание, перемещение, изменение размера и уничтожение окон.

  • Обработка ввода: работа с событиями клавиатуры и мыши.

  • Управление сообщениями: управление циклом сообщений для оконных приложений.

  • Отрисовка: взаимодействие с GDI32.dll для рендеринга графики и текста.

  • Операции с буфером обмена: предоставление функций для копирования и вставки данных.

Большинство приложений Windows, будь то написанные на C++, C# или других языках, зависят от User32.dll для своих UI-компонентов. Когда вы взаимодействуете с окном, кнопкой или меню, приложение вызывает функции внутри User32.dll для обработки соответствующих действий.

User32.dll находится в каталогах C:/windows/SysWow64 (для 64-битных систем) и C:/Windows/system32 (для 32-битных систем).

user32.dll
user32.dll

Создадим новое консольное приложение с именем User32ConsoleApp.

Мы собираемся использовать функцию MessageBox из user32.dll. Это одна из самых популярных функций в user32.dll. Как вы могли догадаться, .NET уже использует её в WinForms и WPF, но немногие знают, что это всего лишь обертка над функцией MessageBox из user32.dll.

При работе с P/Invoke мы просто делегируем функциональность исходному коду (в нашем случае — user32.dll). Это похоже на вызов функции удалённо: вы просто объявляете сигнатуру функции с некоторыми дополнительными атрибутами.

Функция MessageBox в библиотеке User32.dll отображает модальное диалоговое окно с системной иконкой, набором кнопок и сообщением, специфичным для приложения. Она обычно используется для предоставления пользователю информации, предупреждений или ошибок.

Пример на C++:

int WINAPI MessageBox(
    _In_opt_ HWND hWnd,
    _In_     LPCTSTR lpText,
    _In_     LPCTSTR lpCaption,
    _In_     UINT uType
);

Параметры:

  • hWnd: Дескриптор окна-владельца для message box. Если NULL, то у окна нет владельца.

  • lpText: Текст, который будет отображаться в message box.

  • lpCaption: Текст, который будет отображаться в заголовке message box.

  • uType: Целое число, которое указывает содержимое и поведение message box.

Чтобы вызвать функцию MessageBox из user32.dll в нашем приложении на C#, нужно объявить её сигнатуру на C#. Вот как это выглядит:

[DllImport("user32.dll", CharSet = CharSet.Unicode)]
private static extern int MessageBox(
    IntPtr owner,
    string message,
    string title,
    uint type
);

Этот код объявляет метод MessageBox, который позволяет взаимодействовать с неуправляемой функцией MessageBox в библиотеке user32.dll. Вот его ключевые элементы:

  • [DllImport("user32.dll")]: Указывает, что метод импортирован из библиотеки user32.dll.

  • CharSet = CharSet.Unicode: Указывает, что набор символов для строковых параметров должен быть Unicode, что важно для правильной обработки международных символов.

  • Параметры:

    • IntPtr owner: Дескриптор окна-владельца для message box.

    • string message: Текст, который будет отображаться в message box.

    • string title: Заголовок окна message box.

    • uint type: Целое число, определяющее содержимое и поведение окна.

Теперь вызовем эту функцию в нашем C# коде:

static void Main(string[] args)
{
  MessageBox(IntPtr.Zero, "Привет из message box", "Заголовок окна", 0);
}

Вот результат выполнения программы:

C# P/Invoke result
C# P/Invoke result

Мы также можем передать различные значения для четвёртого параметра, чтобы изменить поведение окна:

static void Main(string[] args)
{
    const int MS_OK = 0;
    const int OK_CANCEL = 1;
    const int MS_STOP = 16;

    MessageBox(IntPtr.Zero, "Привет из message box", "Заголовок окна", OK_CANCEL);
}

Написание и экспорт C библиотеки из .NET

В этой секции мы сосредоточимся на написании и вызове C библиотеки из .NET с использованием P/Invoke. Мы будем следовать примерно тем же шагам, что и для интеграции Windows API.

Для начала создадим новую C библиотеку, которая умножает два числа.

Откройте текстовый редактор (можно использовать Notepad), вставьте следующий код и сохраните файл с расширением .c (например, file.c):

int multiply(int a, int b) {
    return a * b;
}

Теперь скомпилируем её. Для этого используйте любой C компилятор. После установки, перейдите в папку с файлом и выполните следующую команду:

gcc -shared -o file.dll file.c

Это создаст динамическую библиотеку (DLL) под именем file.dll.

Интеграция этой библиотеки в .NET выполняется аналогично интеграции Windows API. В том же классе добавим следующие строки:

[DllImport("file.dll")]
private static extern int multiply(int a, int b);

А вот и наш метод Main:

static void Main(string[] args)
{
    int firstNumber = 67;
    int secondNumber = 90;

    Console.WriteLine($"{nameof(firstNumber)} * {nameof(secondNumber)} = {multiply(firstNumber, secondNumber)}");
}

Заключение

P/Invoke, хотя и воспринимается как сложный механизм, является незаменимым инструментом для разработчиков на C#, которые стремятся расширить возможности своих приложений за пределы .NET. Внимательно оценивая его сильные и слабые стороны, разработчики могут эффективно использовать P/Invoke для интеграции устаревшего кода, получения доступа к системным функциям и оптимизации операций, критически важных для производительности.

Комментарии (4)


  1. Xexa
    07.10.2024 09:06
    +4

    Как же поверхностно. Ни о передаче строк в частности, ни о передачи массивов данных в целом оттуда/сюда.

    Даже что есть - в справке MS есть.


  1. Malstream
    07.10.2024 09:06
    +3

    Очередной пересказ документации Microsoft от GPT.

    Ни слова про LibraryImport вместо DLLImport.

    Ни слова про то, что в шарпе давно есть указатели на функции.


  1. MischGun
    07.10.2024 09:06

    Только в system32 находятся 64-разрядные версии, а в SysWOW64 - 32-битные.

    P.S. Не спрашивайте почему так!


  1. Ascar
    07.10.2024 09:06

    ну это не вызов крестов из шарпа, как указано в заголовке на картинке...