Введение
В своей практике я несколько раз сталкивался с задачей интеграции и взаимодействия с низкоуровневыми языками программирования (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-битных систем).
Создадим новое консольное приложение с именем 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);
}
Вот результат выполнения программы:
Мы также можем передать различные значения для четвёртого параметра, чтобы изменить поведение окна:
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)
Malstream
07.10.2024 09:06+3Очередной пересказ документации Microsoft от GPT.
Ни слова про LibraryImport вместо DLLImport.
Ни слова про то, что в шарпе давно есть указатели на функции.
MischGun
07.10.2024 09:06Только в system32 находятся 64-разрядные версии, а в SysWOW64 - 32-битные.
P.S. Не спрашивайте почему так!
Xexa
Как же поверхностно. Ни о передаче строк в частности, ни о передачи массивов данных в целом оттуда/сюда.
Даже что есть - в справке MS есть.