Работа с динамическими библиотеками DLL/so в языке Python для взаимодействия с кодом на C и C++

Раздел: Расширения -> Интеграция с C/C++

Основные подходы к работе с динамическими библиотеками в Python

Как загрузить существующую динамическую библиотеку и вызвать функцию из Python?

Самый простой и встроенный способ — использование модуля ctypes. Он позволяет загружать разделяемые библиотеки (.dll в Windows, .so в Linux) и вызывать экспортированные функции, автоматически преобразуя типы Python в типы C.

Пример загрузки библиотеки math в Linux (libm.so) и вызова функции sqrt:

from ctypes import CDLL

lib = CDLL('libm.so.6')  # или './mylib.dll' на Windows
result = lib.sqrt(25.0)
print(result)  # выведет приблизительно 5.0

Python c import (импорт c библиотек в python)

Пояснение:

  • CDLL загружает библиотеку и считает, что функции используют стандартный вызов C (cdecl). Для Windows с соглашением stdcall используется WinDLL.
  • Тип возвращаемого значения по умолчанию — int. Для функции с double нужно явно задать: lib.sqrt.restype = c_double.
  • Аргументы также можно уточнять: lib.sqrt.argtypes = [c_double].

Типичные ошибки:

  • OSError: [WinError 126] — библиотека не найдена. Проверьте путь, разрядность библиотеки (32/64 бит должна совпадать с Python).
  • Segmentation fault при неправильном задании типов аргументов или возвращаемого значения. Всегда указывайте argtypes и restype.
  • Проблемы с кодировкой строк: функции, ожидающие const char*, требуют преобразования через c_char_p. Если строка должна изменяться, используйте create_string_buffer.

Как безопасно и с поддержкой C99 вызывать функции из динамической библиотеки?

Модуль cffi (C Foreign Function Interface) предлагает два подхода: быструю загрузку существующих библиотек (ABI) и генерацию C-кода (API). Он лучше контролирует типы и не требует ручного задания argtypes.

Пример использования cffi для вызова функции sqrt из libm:

from cffi import FFI

ffi = FFI()
ffi.cdef('double sqrt(double x);')
lib = ffi.dlopen('libm.so.6')
print(lib.sqrt(25.0))

динамические библиотеки python (динамические библиотеки (dll/so) в python)

Пояснение:

  • Метод cdef описывает заголовки функций на языке C.
  • dlopen загружает библиотеку.
  • Типы контролируются автоматически, ошибки преобразования практически исключены.

Ошибки:

  • ImportError: no module named '_cffi_backend' — не установлен CFFI. Установите через pip.
  • При использовании API-режима (из C-файлов) требуется компилятор; ABI-режим обходится без него.
  • Библиотеки с нестандартными соглашениями вызова могут потребовать ручного указания ffi.dlopen(..., flags=...).

Как создать собственное расширение Python на C++ и получить динамическую библиотеку (.pyd / .so)?

Библиотека pybind11 упрощает создание расширений из C++. Она генерирует код, который компилируется в разделяемую библиотеку, импортируемую как обычный модуль Python.

Пример: простой модуль с функцией, возвращающей сумму двух чисел:

#include <pybind11/pybind11.h>

int add(int a, int b) {
    return a + b;
}

PYBIND11_MODULE(myadd, m) {
    m.doc() = "My add module";
    m.def("add", &add, "Adds two integers");
}

Файл setup.py для сборки:

from setuptools import setup
from pybind11.setup_helpers import Pybind11Extension

ext_module = Pybind11Extension('myadd', ['myadd.cpp'])
setup(name='myadd', ext_modules=[ext_module])

Сборка и установка:

python setup.py build_ext --inplace

После этого модуль можно импортировать:

import myadd
print(myadd.add(2, 3))  # 5

Типичные проблемы:

  • Ошибки линковки при отсутствии pybind11 или неправильном пути к заголовкам. Установите pybind11 через pip.
  • Конфликт соглашений вызова (cdecl vs stdcall) актуален только для Windows; pybind11 использует __declspec(dllexport) автоматически.
  • Для работы с многопоточностью в функциях C++ нужно освобождать GIL (pybind11 предоставляет макросы).

Как ускорить код Python, компилируя его в динамическую библиотеку с помощью Cython?

Cython транслирует Python-подобный код в C, который затем компилируется в разделяемую библиотеку. Это подход для случаев, когда нужно получить высокую производительность без написания чистого C.

Пример: файл fast_loop.pyx с функцией, вычисляющей сумму квадратов:

def sum_squares(int n):
    cdef int i, s = 0
    for i in range(n):
        s += i * i
    return s

Файл setup.py:

from setuptools import setup
from Cython.Build import cythonize

setup(ext_modules=cythonize('fast_loop.pyx'))

Сборка:

python setup.py build_ext --inplace

Использование:

import fast_loop
print(fast_loop.sum_squares(1000))

Ошибки при работе с Cython:

  • Cython.Compiler.Errors.CompileError — синтаксические ошибки в .pyx файле. Проверьте типы и отступы.
  • Неправильное объявление типов может привести к снижению производительности или ошибкам сегментации.
  • Для использования внешних библиотек C внутри Cython нужно явно объявлять их в cdef extern from.

Расширенные примеры работы с динамическими библиотеками

Пример 1: Передача массивов в функцию C через ctypes (с обработкой numpy)

Часто требуется передать большой массив чисел в C-функцию. Используем numpy.ctypes для получения указателя на данные.

Пример
import numpy as np
from ctypes import CDLL, c_double, c_int

lib = CDLL('./mystuff.so')
lib.process_array.argtypes = [POINTER(c_double), c_int]
lib.process_array.restype = None

data = np.array([1.0, 2.0, 3.0, 4.0], dtype=np.float64)
ptr = data.ctypes.data_as(POINTER(c_double))
lib.process_array(ptr, len(data))
print(data)  # массив будет изменён in-place

Результат (вывод изменённого массива):

[2.0, 4.0, 6.0, 8.0]

Пояснение: C-функция process_array удваивает каждый элемент. Использование POINTER(c_double) позволяет передать указатель на буфер numpy без копирования.

Пример 2: Регистрация callback-функции Python в C-библиотеке (ctypes + ctypes.CFUNCTYPE)

Допустим, C-библиотека ожидает указатель на функцию-обработчик. Python может передать свою функцию с помощью CFUNCTYPE.

Пример
from ctypes import CDLL, CFUNCTYPE, c_int

# Предположим, в библиотеке определена функция:
# void register_callback(void (*f)(int));

CALLBACK = CFUNCTYPE(None, c_int)
lib = CDLL('./callbacks.dll')
lib.register_callback.argtypes = [CALLBACK]
lib.register_callback.restype = None

def my_callback(value):
    print(f"Callback called with {value}")

callback_func = CALLBACK(my_callback)
lib.register_callback(callback_func)
# Далее C-код может вызывать my_callback

Результат (при вызове из C):

Callback called with 42

Важно: объект callback_func должен существовать, пока библиотека может его использовать. Иначе Python garbage collector удалит его, что приведёт к краху.

Пример 3: Создание класса C++ с методами и свойствами через pybind11

Покажем, как обернуть простой класс Counter с конструктором, методом и свойством.

Пример
#include <pybind11/pybind11.h>

class Counter {
public:
    Counter(int start) : count(start) {}
    void increment(int step = 1) { count += step; }
    int get_count() const { return count; }
private:
    int count;
};

namespace py = pybind11;
PYBIND11_MODULE(counter, m) {
    py::class_<Counter>(m, "Counter")
        .def(py::init<int>())
        .def("increment", &Counter::increment, py::arg("step") = 1)
        .def("get_count", &Counter::get_count)
        .def_property_readonly("count", &Counter::get_count);
}

Сборка аналогично примеру выше. Использование в Python:

Пример
import counter
c = counter.Counter(10)
c.increment(5)
print(c.count)  # 15

Результат:

15

Обратите внимание: def_property_readonly создаёт атрибут только для чтения. Для изменяемых свойств используется def_property.

Пример 4: Оптимизация цикла с помощью Cython и внешней математической библиотеки

Допустим, нужно быстро вычислить сумму синусов большого массива. Используем Cython с вызовом sin из libm.

Пример
# fast_sum.pyx
cdef extern from "math.h" nogil:
    double sin(double x)

def fast_sin_sum(double[:] arr):
    cdef Py_ssize_t i
    cdef double s = 0.0
    for i in range(arr.shape[0]):
        s += sin(arr[i])
    return s

Для компиляции потребуется указать библиотеку math при линковке (в setup.py):

Пример
from setuptools import setup
from Cython.Build import cythonize

setup(ext_modules=cythonize(
    'fast_sum.pyx',
    libraries=['m']  # линковка с libm
))

Тест:

Пример
import numpy as np
import fast_sum
data = np.linspace(0, np.pi, 1000000, dtype=np.float64)
print(fast_sum.fast_sin_sum(data))

Результат (приблизительное значение):

1999999.9999999998

Cython автоматически использует быстрый доступ к памяти double[:] и освобождает GIL (nogil), что даёт почти скорость C.

Динамические библиотеки (DLL/so) в Python - comments

En
динамические библиотеки python (python)