Создание модулей расширения CPython на языке C
Основные подходы для создания расширений на C
Как разработать Python-расширение на C с максимальной производительностью?
Python C API предоставляет прямой доступ к внутренностям интерпретатора. Это наиболее эффективный способ создания расширений, но требует глубокого понимания управления памятью и типами CPython.
Цель: Создать модуль mymath, содержащий функцию add(x,y), возвращающую сумму.
Шаг 1. Создайте файл mymath.c:
#include <Python.h>
static PyObject* mymath_add(PyObject* self, PyObject* args) {
double a, b;
if (!PyArg_ParseTuple(args, "dd", &a, &b))
return NULL;
double result = a + b;
return PyFloat_FromDouble(result);
}
static PyMethodDef MyMathMethods[] = {
{"add", mymath_add, METH_VARARGS, "Add two numbers"},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef mymathmodule = {
PyModuleDef_HEAD_INIT,
"mymath",
NULL,
-1,
MyMathMethods
};
PyMODINIT_FUNC PyInit_mymath(void) {
return PyModule_Create(&mymathmodule);
}модули расширения python (модули расширения python (c extensions))
Шаг 2. Создайте setup.py для сборки:
from setuptools import setup, Extension
module = Extension('mymath', sources=['mymath.c'])
setup(name='mymath',
version='1.0',
description='Example extension',
ext_modules=[module])
Шаг 3. Соберите и установите: python setup.py build_ext --inplace
Шаг 4. Проверьте в Python:
import mymath
print(mymath.add(2.5, 3.7))
6.2
Типичные проблемы:
- Ошибка линковки из-за отсутствия Python.h. Установите python-dev или python3-dev.
- Несоответствие типов в PyArg_ParseTuple. Используйте правильные коды формата (d – double, i – int, s – string).
- Утечки памяти: при создании новых объектов Python вызывайте Py_DECREF при необходимости.
- Краш при импорте на Windows из-за неправильного экспорта символов. Добавьте __declspec(dllexport) в PyInit_ функцию.
Как упростить создание расширения с помощью Cython?
Cython позволяет писать код на Python с аннотациями типов и транслировать его в C. Это значительно проще, чем ручное написание C API, и даёт почти нативную производительность.
Пример: тот же модуль mymath.
# mymath.pyx
def add(double a, double b):
return a + b
# setup.py
from setuptools import setup
from Cython.Build import cythonize
setup(ext_modules=cythonize("mymath.pyx"))
Сборка: python setup.py build_ext --inplace
Использование идентично.
Проблемы: Cython генерирует большой .c файл; отладка требует сравнения сгенерированного кода; статические типы могут ограничивать динамические возможности Python.
Как вызывать C-функции без написания полноценного расширения?
ctypes – стандартная библиотека Python для вызова разделяемых библиотек. Не требует компиляции расширения, но менее производительная из-за накладных расходов на преобразование типов.
Пример: предположим, у нас есть библиотека libmymath.so с функцией double add(double, double).
from ctypes import CDLL
lib = CDLL("./libmymath.so")
lib.add.argtypes = [ctypes.c_double, ctypes.c_double]
lib.add.restype = ctypes.c_double
print(lib.add(2.5, 3.7))
6.2
Проблемы: Необходимость вручную определять argtypes и restype; ошибки сегментации при несовпадении типов; сложно работать со сложными структурами и массивами.
Как интегрировать C-библиотеку с помощью CFFI?
CFFI (C Foreign Function Interface) – более мощная альтернатива ctypes, поддерживает встраивание C-кода прямо в Python и автоматическую генерацию обёрток.
from cffi import FFI
ffi = FFI()
# Описываем сигнатуру
ffi.cdef("double add(double, double);")
# Загружаем библиотеку
lib = ffi.dlopen("./libmymath.so")
result = lib.add(2.5, 3.7)
print(result)
6.2
Проблемы: Требуется установка пакета cffi; для сложных ABI может потребоваться написание .h файлов.
Расширенные примеры и нестандартные случаи
Пример 1: Модуль с обработкой списков (Python C API)
Создадим функцию sum_list, принимающую список чисел и возвращающую сумму всех элементов.
#include <Python.h>
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 must contain numbers");
return NULL;
}
}
return PyFloat_FromDouble(total);
}
// ... (таблица методов и инициализация аналогично)
Сборка и тест:
import mymath
print(mymath.sum_list([1.5, 2, 3.2, 4]))
10.7
Пример 2: Cython с typed memoryview для быстрой обработки массивов
# sum_array.pyx
def sum_array(double[:] arr):
cdef:
int i
double total = 0
for i in range(arr.shape[0]):
total += arr[i]
return total
Использование:
import numpy as np
import sum_array
arr = np.array([1.0, 2.0, 3.0], dtype=np.float64)
print(sum_array.sum_array(arr))
6.0
Пример 3: ctypes – работа со структурами и указателями
from ctypes import Structure, c_double, POINTER
class Point(Structure):
_fields_ = [("x", c_double), ("y", c_double)]
lib = CDLL("./libgeom.so")
# Функция double distance(Point* p1, Point* p2)
lib.distance.argtypes = [POINTER(Point), POINTER(Point)]
lib.distance.restype = c_double
p1 = Point(0.0, 0.0)
p2 = Point(3.0, 4.0)
print(lib.distance(p1, p2))
5.0
Пример 4: CFFI с обратными вызовами (callback)
from cffi import FFI
ffi = FFI()
ffi.cdef("""
typedef double (*callback_t)(double);
double apply_callback(callback_t cb, double x);
""")
lib = ffi.dlopen("./libcallback.so")
# Создаём callback на Python
@ffi.callback("double(double)")
def square(x):
return x * x
result = lib.apply_callback(square, 5.0)
print(result)
25.0