Разработка C-модулей для интерпретатора Python

Раздел: Python -> Расширения Python

Создание C-расширения с помощью Python C API

Как создать минимальный C-модуль для Python?

Основной способ интеграции C-кода с Python заключается в использовании официального Python C API. Этот подход даёт максимальную производительность и полный контроль над памятью и типами. Для создания модуля требуется написать файл на C, который определяет функции модуля, таблицу методов и инициализацию. Затем модуль компилируется с помощью setuptools (distutils) в динамическую библиотеку (.so или .pyd).

#include <Python.h>

// Функция, складывающая два целых числа
static PyObject* example_add(PyObject* self, PyObject* args) {
    int a, b;
    if (!PyArg_ParseTuple(args, "ii", &a, &b)) {
        return NULL;
    }
    return PyLong_FromLong(a + b);
}

// Таблица методов модуля
static PyMethodDef ExampleMethods[] = {
    {"add", example_add, METH_VARARGS, "Add two integers"},
    {NULL, NULL, 0, NULL}
};

// Определение модуля
static struct PyModuleDef examplemodule = {
    PyModuleDef_HEAD_INIT,
    "example",
    NULL,
    -1,
    ExampleMethods
};

// Инициализация модуля
PyMODINIT_FUNC PyInit_example(void) {
    return PyModule_Create(&examplemodule);
}

Python c module (c-расширения для python (c module))

Далее создаётся файл setup.py:

from setuptools import setup, Extension

module = Extension('example', sources=['example.c'])

setup(name='example',
      version='1.0',
      description='Example C extension',
      ext_modules=[module])

Компиляция выполняется командой python setup.py build_ext --inplace. После сборки модуль можно импортировать и использовать:

import example
print(example.add(3, 5))  # 8

Какие типичные ошибки возникают при сборке?

  • Ошибка ссылок на неопределённые символы – необходимо убедиться, что все используемые функции C API объявлены (подключайте Python.h первым).
  • Segfault при неверной передаче аргументов – всегда проверяйте результат PyArg_ParseTuple.
  • Утечка памяти – не забывайте увеличивать счётчик ссылок для объектов, возвращаемых из C (Py_INCREF) и уменьшать для переданных (Py_DECREF).

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

Как использовать Cython для создания расширений?

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

def add(int a, int b):
    return a + b

И соответствующий setup.py:

from setuptools import setup
from Cython.Build import cythonize

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

После компиляции получается C-расширение с тем же интерфейсом. Cython особенно полезен для оптимизации циклов и работы с массивами (numpy).

Проблема: при использовании Python-объектов внутри Cython может наблюдаться накладные расходы на преобразование типов. Решение – использовать статические типы и декларации.

Как вызывать C-функции без написания модуля с помощью ctypes?

ctypes – встроенная библиотека, позволяющая динамически загружать разделяемые библиотеки (.so/.dll) и вызывать функции с описанием сигнатуры. Не требуется компиляция модуля Python.

# libmath.so – библиотека на C, содержащая функцию int add(int, int)
import ctypes

lib = ctypes.CDLL('./libmath.so')
lib.add.argtypes = (ctypes.c_int, ctypes.c_int)
lib.add.restype = ctypes.c_int
print(lib.add(3, 5))  # 8

Подход удобен для простых вызовов, но не даёт прямого доступа к объектам Python и менее производителен из-за накладных расходов на преобразование типов.

Ошибка: неверное указание типов приводит к непредсказуемому поведению (segfault). Всегда проверяйте соглашение о вызове (C vs stdcall).

Как использовать cffi для генерации расширений?

cffi (C Foreign Function Interface) предоставляет два режима: ABИ (вызов скомпилированной библиотеки) и API (компиляция модуля на лету). Пример ABИ:

from cffi import FFI

ffi = FFI()
ffi.cdef("int add(int, int);")
lib = ffi.dlopen('./libmath.so')
print(lib.add(3, 5))  # 8

cffi более безопасен (проверка типов во время выполнения) и поддерживает работу с памятью, структурами. В режиме API можно сгенерировать и скомпилировать C-расширение автоматически.

Проблема: при использовании сложных структур необходимо вручную описывать каждый элемент. Решение – использовать генерацию кода на основе заголовочных файлов.

Как упростить создание расширений с помощью pybind11?

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

#include <pybind11/pybind11.h>
namespace py = pybind11;

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

PYBIND11_MODULE(example, m) {
    m.def("add", &add, "Add two integers");
}

Сборка через pybind11 требует только правильно настроенный setup.py с указанием include paths. pybind11 рекомендуется для современных проектов на C++.

Проблема: pybind11 зависит от C++11/14/17, может увеличить время сборки. Решение – использовать статическую линковку или precompiled headers.

Расширенные примеры C-расширений

1. Обработка строк и управление памятью

Функция, принимающая строку и возвращающая её длину:

Пример
static PyObject* example_len(PyObject* self, PyObject* args) {
    const char* s;
    if (!PyArg_ParseTuple(args, "s", &s))
        return NULL;
    return PyLong_FromSize_t(strlen(s));
}
import example
print(example.len('Hello'))  # 5

Важно: строка копируется в C-буфер; для больших данных используйте s# (с длиной).

2. Создание класса с методами

Определение нового типа Counter:

Пример
typedef struct {
    PyObject_HEAD
    long count;
} Counter;

static int Counter_init(Counter* self, PyObject* args, PyObject* kwds) {
    self->count = 0;
    return 0;
}

static PyObject* Counter_increment(Counter* self, PyObject* args) {
    self->count++;
    return PyLong_FromLong(self->count);
}

static PyMethodDef Counter_methods[] = {
    {"increment", (PyCFunction)Counter_increment, METH_NOARGS, "Increment counter"},
    {NULL}
};

static PyTypeObject CounterType = {
    PyVarObject_HEAD_INIT(NULL, 0)
    .tp_name = "example.Counter",
    .tp_basicsize = sizeof(Counter),
    .tp_itemsize = 0,
    .tp_flags = Py_TPFLAGS_DEFAULT,
    .tp_methods = Counter_methods,
    .tp_init = (initproc)Counter_init,
};

Регистрация типа в модуле:

Пример
PyMODINIT_FUNC PyInit_example(void) {
    PyObject* m = PyModule_Create(&examplemodule);
    if (PyType_Ready(&CounterType) < 0) return NULL;
    Py_INCREF(&CounterType);
    PyModule_AddObject(m, "Counter", (PyObject*)&CounterType);
    return m;
}
c = example.Counter()
print(c.increment())  # 1
print(c.increment())  # 2

Утечка памяти: при добавлении типа в модуль необходимо Py_INCREF, иначе объект может быть уничтожен до завершения модуля.

3. Работа с GIL в многопоточных расширениях

Для выполнения длительных C-вычислений без блокировки GIL используйте Py_BEGIN_ALLOW_THREADS / Py_END_ALLOW_THREADS:

Пример
static PyObject* compute_heavy(PyObject* self, PyObject* args) {
    long n;
    if (!PyArg_ParseTuple(args, "l", &n)) return NULL;
    long result = 0;
    Py_BEGIN_ALLOW_THREADS
    // Долгий расчёт (n итераций)
    for (long i = 0; i < n; i++) {
        result += i;
    }
    Py_END_ALLOW_THREADS
    return PyLong_FromLong(result);
}

Важно: внутри освобождённого блока нельзя вызывать Python API, иначе возникнет deadlock.

4. Обработка исключений Python

Установка собственного исключения:

Пример
static PyObject* ExampleError = NULL;

static PyObject* divide(PyObject* self, PyObject* args) {
    double a, b;
    if (!PyArg_ParseTuple(args, "dd", &a, &b)) return NULL;
    if (b == 0.0) {
        PyErr_SetString(ExampleError, "Division by zero");
        return NULL;
    }
    return PyFloat_FromDouble(a / b);
}

PyMODINIT_FUNC PyInit_example(void) {
    PyObject* m = PyModule_Create(&examplemodule);
    ExampleError = PyErr_NewException("example.ExampleError", NULL, NULL);
    Py_INCREF(ExampleError);
    PyModule_AddObject(m, "ExampleError", ExampleError);
    return m;
}
import example
try:
    example.divide(10, 0)
except example.ExampleError as e:
    print(e)  # Division by zero

5. Работа с последовательностями (списки, кортежи)

Функция, суммирующая элементы списка:

Пример
static PyObject* sum_list(PyObject* self, PyObject* args) {
    PyObject* list;
    if (!PyArg_ParseTuple(args, "O!", &PyList_Type, &list))
        return NULL;
    Py_ssize_t len = PyList_Size(list);
    double total = 0.0;
    for (Py_ssize_t i = 0; i < len; i++) {
        PyObject* item = PyList_GetItem(list, i);
        if (PyFloat_Check(item))
            total += PyFloat_AsDouble(item);
        else if (PyLong_Check(item))
            total += (double)PyLong_AsLong(item);
        else {
            PyErr_SetString(PyExc_TypeError, "List items must be numbers");
            return NULL;
        }
    }
    return PyFloat_FromDouble(total);
}
print(example.sum_list([1, 2.5, 3]))  # 6.5

Ошибка: PyList_GetItem возвращает временную ссылку, не требующую уменьшения счётчика. Если ссылку сохранять, необходимо Py_INCREF.

C-расширения для Python (C module) - comments

En
Python c module (python)