Взаимодействие Python с DLL: инструменты системного программирования
Введение
При разработке системного программного обеспечения на Python часто возникает необходимость вызывать функции, реализованные в динамически подключаемых библиотеках (DLL) для Windows или разделяемых объектах (so) для Linux. Это требуется для работы с низкоуровневыми API операционной системы, аппаратными драйверами или унаследованными модулями на C. Рассмотрим основные библиотеки и подходы, позволяющие загружать DLL и взаимодействовать с ними из Python.
Как вызвать функцию из DLL с помощью стандартной библиотеки ctypes?
ctypes - встроенная библиотека Python, не требующая дополнительной установки. Она предоставляет полный контроль над типами данных и выравниванием структур. Это универсальное и наиболее эффективное решение для простых сценариев интеграции с C-кодом.
Пример вызова функции MessageBox из user32.dll:
import ctypes
# Загружаем библиотеку
user32 = ctypes.WinDLL('user32', use_last_error=True)
# Определяем прототип функции: int MessageBoxW(HWND, LPCWSTR, LPCWSTR, UINT)
user32.MessageBoxW.argtypes = [ctypes.c_void_p, ctypes.c_wchar_p, ctypes.c_wchar_p, ctypes.c_uint]
user32.MessageBoxW.restype = ctypes.c_int
# Вызываем
result = user32.MessageBoxW(None, "Привет из Python!", "Заголовок", 0)
print(f"Результат: {result}")Python c code (примеры вызова c из python)
Пояснение шагов:
- Импорт библиотеки ctypes.
- Загрузка DLL с помощью WinDLL (для stdcall) или CDLL (для cdecl). Параметр use_last_error позволяет получить код ошибки через GetLastError().
- Задание argtypes - кортеж типов аргументов. Это улучшает проверку и автоматическое преобразование типов.
- Задание restype - тип возвращаемого значения.
- Вызов функции с корректными аргументами (None для NULL-дескриптора окна).
Типичные ошибки:
- Ошибка загрузки DLL (FileNotFoundError) - проверьте путь или используйте абсолютный путь.
- Ошибка соглашения о вызовах - используйте CDLL для cdecl (например, msvcrt) и WinDLL для stdcall.
- Несоответствие типов - задавайте argtypes явно, особенно для строк (c_char_p для char*, c_wchar_p для wchar_t*).
- Ошибки с возвращаемыми указателями - используйте ctypes.POINTER и соответствующие типы.
Как упростить описание структур и функций с помощью cffi?
cffi (C Foreign Function Interface) - внешняя библиотека, предоставляющая более высокий уровень абстракции и удобный синтаксис для объявления C-интерфейсов. Она поддерживает работу с существующими DLL/so и позволяет писать C-вставки непосредственно в строке Python.
Пример вызова той же MessageBox с использованием cffi API (режим ABI):
from cffi import FFI
ffi = FFI()
# Объявляем прототип функции
ffi.cdef("""
int MessageBoxW(void* hwnd, const wchar_t* lpText, const wchar_t* lpCaption, unsigned int uType);
""")
# Загружаем библиотеку
user32 = ffi.dlopen('user32.dll')
# Вызываем (преобразуем строки в wchar_t*)
result = user32.MessageBoxW(ffi.NULL, ffi.new('const wchar_t[]', 'Привет из cffi!'), ffi.new('const wchar_t[]', 'Заголовок'), 0)
print(f"Результат: {result}")C вызов python (вызов python из c)
Пояснение:
- Создание объекта FFI.
- Объявление C-функции через строку - это напоминает заголовочный файл.
- Загрузка библиотеки с помощью dlopen.
- Вызов функции с использованием ffi.new для создания C-массивов или строк с нулевым байтом.
Проблемы и решения:
- Требуется установка: pip install cffi.
- Сложность с динамическими библиотеками, где имена символов могут иметь манглинг (например, при экспорте C++ функций) - cffi лучше работает с C-совместимыми экспортами.
- При работе с большим количеством функций удобнее использовать cffi в режиме API, генерируя модуль из C-заголовков.
Когда полезен pybind11 для интеграции с C++?
pybind11 - не столько библиотека для загрузки существующих DLL, сколько инструмент для создания Python-модулей на основе C++ кода. Он позволяет обернуть любые C++ классы и функции в Python-модули, которые затем импортируются как обычные питоновские модули. Это эффективно, если у вас есть исходный код на C++ и вы хотите предоставить Python-интерфейс к нему. Для загрузки готовой DLL с неизвестным исходным кодом pybind11 не применяется.
Пример синтаксиса обёртки (C++):
#include <pybind11/pybind11.h>
int add(int a, int b) { return a + b; }
PYBIND11_MODULE(example, m) {
m.def("add", &add, "A function which adds two numbers");
}
библиотека dll python (библиотека для работы с dll в python)
После компиляции получается файл example.pyd, который импортируется в Python:
import example
print(example.add(3, 5)) # 8Проблемы: требует компилятора и настройки системы сборки (CMake, setuptools). Не подходит для случая, когда есть только бинарная DLL без исходников.
Как найти DLL в системе с помощью ctypes.util?
Иногда имя DLL известно, но полный путь к ней зависит от среды. Библиотека ctypes.util предоставляет функцию find_library, которая ищет библиотеку в стандартных каталогах (например, PATH). Пример:
import ctypes.util
lib_path = ctypes.util.find_library('msvcrt')
print(lib_path) # Linux: 'libc.so.6', Windows: 'msvcrt.dll'
# Загружаем
if lib_path:
lib = ctypes.CDLL(lib_path)
else:
print('Библиотека не найдена')На Windows find_library работает не со всеми DLL (только с теми, что перечислены в реестре). Для сторонних DLL лучше указывать полный путь или использовать относительный путь к исполняемому скрипту.
Расширенные примеры
Работа с функциями, принимающими структуры
Рассмотрим вызов функции GetSystemTime из kernel32, которая заполняет структуру SYSTEMTIME.
import ctypes
from ctypes import wintypes
# Определяем структуру
class SYSTEMTIME(ctypes.Structure):
_fields_ = [
('wYear', wintypes.WORD),
('wMonth', wintypes.WORD),
('wDayOfWeek', wintypes.WORD),
('wDay', wintypes.WORD),
('wHour', wintypes.WORD),
('wMinute', wintypes.WORD),
('wSecond', wintypes.WORD),
('wMilliseconds', wintypes.WORD),
]
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
kernel32.GetSystemTime.argtypes = [ctypes.POINTER(SYSTEMTIME)]
kernel32.GetSystemTime.restype = None
st = SYSTEMTIME()
kernel32.GetSystemTime(ctypes.byref(st))
print(f"{st.wDay}.{st.wMonth}.{st.wYear} {st.wHour}:{st.wMinute}:{st.wSecond}")
25.3.2025 14:35:12
Пояснение: структура объявляется как класс, наследующий от ctypes.Structure, с полями _fields_. Для передачи по ссылке используется ctypes.byref() или pointer().
Передача массивов и указателей
Функция memcpy из msvcrt копирует блок памяти. Покажем, как передать изменяемый буфер.
import ctypes
msvcrt = ctypes.CDLL('msvcrt')
msvcrt.memcpy.argtypes = [ctypes.c_void_p, ctypes.c_void_p, ctypes.c_size_t]
msvcrt.memcpy.restype = ctypes.c_void_p
# Создаём исходные данные (строка) и целевой буфер
src = ctypes.create_string_buffer(b"Hello, DLL!")
dst = ctypes.create_string_buffer(20)
msvcrt.memcpy(dst, src, len(src))
print(dst.value) # b"Hello, DLL!"
b"Hello, DLL!"
Для работы с изменяемыми массивами используйте create_string_buffer (для char*) или create_unicode_buffer (для wchar_t*).
Обработка ошибок и исключений при вызове DLL
При вызове функций могут возникать Windows-ошибки. Используйте GetLastError и блок try/except.
import ctypes
kernel32 = ctypes.WinDLL('kernel32', use_last_error=True)
# Попробуем открыть несуществующий файл
CreateFile = kernel32.CreateFileW
CreateFile.argtypes = [ctypes.c_wchar_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_void_p, ctypes.c_uint32, ctypes.c_uint32, ctypes.c_void_p]
CreateFile.restype = ctypes.c_void_p
INVALID_HANDLE_VALUE = ctypes.c_void_p(-1).value
handle = CreateFile('nonexistent.txt', 0x80000000, 0, None, 3, 0, None)
if handle == INVALID_HANDLE_VALUE:
err = ctypes.get_last_error()
raise ctypes.WinError(err)
else:
kernel32.CloseHandle(handle)
OSError: [WinError 2] The system cannot find the file specified
Использование WinError преобразует код ошибки в понятное сообщение. Для проверки ошибок в cffi аналогично можно получить код последней ошибки через ffi.errno (только для errno).
Вызов функций с колбэками (callback)
Многие DLL-функции принимают указатель на функцию обратного вызова (callback). Пример с функцией EnumWindows:
import ctypes
# Определяем тип колбэка
WNDENUMPROC = ctypes.WINFUNCTYPE(ctypes.c_bool, ctypes.c_void_p, ctypes.c_void_p)
user32 = ctypes.WinDLL('user32')
user32.EnumWindows.argtypes = [WNDENUMPROC, ctypes.c_void_p]
user32.EnumWindows.restype = ctypes.c_bool
# Счётчик окон
count = 0
def enum_callback(hwnd, lParam):
global count
count += 1
print(f"Окно: {hwnd}")
return True # продолжать перечисление
callback = WNDENUMPROC(enum_callback)
user32.EnumWindows(callback, None)
print(f"Всего окон: {count}")
Окно: 123456 Окно: 789012 ... Всего окон: 42
Внимание: колбэк не должен вызывать исключения в Python - это может привести к крашу процесса. Используйте try/except внутри колбэка.
Работа с динамическими библиотеками в Linux (so)
Принципы те же, но для загрузки используется CDLL (по умолчанию cdecl). Пример вызова printf из libc:
import ctypes
libc = ctypes.CDLL('libc.so.6')
libc.printf.argtypes = [ctypes.c_char_p]
libc.printf.restype = ctypes.c_int
libc.printf(b"Hello from libc!\n")
Hello from libc!
Для поиска библиотеки используйте ctypes.util.find_library('c').