Взаимодействие Python с DLL: инструменты системного программирования

Раздел: Системное программирование -> Интеграция с C

Введение

При разработке системного программного обеспечения на 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)

Пояснение шагов:

  1. Импорт библиотеки ctypes.
  2. Загрузка DLL с помощью WinDLL (для stdcall) или CDLL (для cdecl). Параметр use_last_error позволяет получить код ошибки через GetLastError().
  3. Задание argtypes - кортеж типов аргументов. Это улучшает проверку и автоматическое преобразование типов.
  4. Задание restype - тип возвращаемого значения.
  5. Вызов функции с корректными аргументами (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)

Пояснение:

  1. Создание объекта FFI.
  2. Объявление C-функции через строку - это напоминает заголовочный файл.
  3. Загрузка библиотеки с помощью dlopen.
  4. Вызов функции с использованием 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').

Библиотека для работы с DLL в Python - comments

En
библиотека dll python (python)