Разработка C-модулей для интерпретатора 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.