Продолжаем тему как вызывать C/C++ из Python3. Теперь используем C API для создания модуля, на этом примере мы сможем разобраться как работает cffi и прочие библиотеки упрощающие нам жизнь. Потому что на мой взгляд это самый трудный способ.
C
Тестовая библиотека для демонстрации работы с глобальными переменными, структурами и функциями с аргументами различных типов. В своих статьях использую вариации одной и той же библиотеки, в зависимости от способа которым я теперь пользуюсь. Ссылки на предыдущие способы внизу.
test.h
typedef struct test_st_s test_st_t;
extern int a;
extern double b;
extern char c;
static PyObject *func_hello(PyObject *self, PyObject *args);
static PyObject *func_ret_int(PyObject *self, PyObject *args);
static PyObject *func_ret_double(PyObject *self, PyObject *args);
static PyObject *func_ret_str(PyObject *self, PyObject *args);
static PyObject *func_many_args(PyObject *self, PyObject *args);
static PyObject *func_ret_struct(PyObject *self, PyObject *args);
struct test_st_s {
PyObject_HEAD // Макрос объявления нового типа, объекта фиксированного размера
int val1;
double val2;
char val3;
};
test.c
// Список функций модуля
static PyMethodDef methods[] = {
{"func_hello", func_hello, METH_NOARGS, "func_hello"}, // Функция без аргументов
{"func_ret_int", func_ret_int, METH_VARARGS, "func_ret_int"}, // Функция с аргументами
{"func_ret_double", func_ret_double, METH_VARARGS, "func_ret_double"},
{"func_ret_str", func_ret_str, METH_VARARGS, "func_ret_str"},
{"func_many_args", func_many_args, METH_VARARGS, "func_many_args"},
{"func_ret_struct", func_ret_struct, METH_VARARGS, "func_ret_struct"},
{NULL, NULL, 0, NULL}
};
// Описание модуля
static struct PyModuleDef module = {
PyModuleDef_HEAD_INIT, "_test", "Test module", -1, methods
};
// Инициализация модуля
PyMODINIT_FUNC
PyInit__test(void) {
PyObject *mod = PyModule_Create(&module);
// Добавляем глобальные переменные
PyModule_AddObject(mod, "a", PyLong_FromLong(a)); // int
PyModule_AddObject(mod, "b", PyFloat_FromDouble(b)); // double
PyModule_AddObject(mod, "c", Py_BuildValue("b", c)); // char
// Добавляем структуру
// Завершение инициализации структуры
if (PyType_Ready(&test_st_t_Type) < 0)
return NULL;
Py_INCREF(&test_st_t_Type);
PyModule_AddObject(mod, "test_st_t", (PyObject *) &test_st_t_Type);
return mod;
}
/**
* Тестовые функции, тестовые переменные.
*/
int a = 5;
double b = 5.12345;
char c = 'X'; // 88
static PyObject *
func_hello(PyObject *self, PyObject *args) { // Можно без args, но будет warning при компиляции.
puts("Hello!");
Py_RETURN_NONE;
}
/**
* Получение значения переменной содержащей значение типа int и возврат его.
*/
static PyObject *
func_ret_int(PyObject *self, PyObject *args) {
int val;
// Проверка кол-ва аргументов
if (PyTuple_Size(args) != 1) {
PyErr_SetString(self, "func_ret_int args error");
}
PyArg_ParseTuple(args, "i", &val);
/*
* Альтернативный вариант.
*
// Получаем аргумент
PyObject *obj = PyTuple_GetItem(args, 0);
// Проверяем его на тип int/long
if (PyLong_Check(obj)) {
PyErr_Print();
}
// Приводим (PyObject *) к int
val = _PyLong_AsInt(obj);
*/
printf("C get func_ret_int: %d\n", val);
return Py_BuildValue("i", val);
}
/**
* Получение значения переменной содержащей значение типа double и возврат его.
*/
static PyObject *
func_ret_double(PyObject *self, PyObject *args) {
double val;
if (PyTuple_Size(args) != 1) {
PyErr_SetString(self, "func_ret_double args error");
}
PyArg_ParseTuple(args, "d", &val);
printf("C get func_ret_double: %f\n", val);
return Py_BuildValue("f", val);
}
/**
* Получение string и возврат его.
*/
static PyObject *
func_ret_str(PyObject *self, PyObject *args) {
char *val;
if (PyTuple_Size(args) != 1) {
PyErr_SetString(self, "func_ret_str args error");
}
PyArg_ParseTuple(args, "s", &val);
/*
* Альтернативный вариант.
*
PyObject *obj = PyTuple_GetItem(args, 0);
PyObject* pResultRepr = PyObject_Repr(obj);
val = PyBytes_AS_STRING(PyUnicode_AsEncodedString(pResultRepr, "utf-8", "ERROR"));
*/
printf("C get func_ret_str: %s\n", val);
return Py_BuildValue("s", val);
}
/**
* Получение значения переменных содержащих значения типа int, double, char *.
*/
static PyObject *
func_many_args(PyObject *self, PyObject *args) {
int val1;
double val2;
char *val3;
if (PyTuple_Size(args) != 3) {
PyErr_SetString(self, "func_ret_str args error");
}
PyArg_ParseTuple(args, "ids", &val1, &val2, &val3);
printf("C get func_many_args: int - %d, double - %f, string - %s\n", val1, val2, val3);
return Py_BuildValue("ifs", val1, val2, val3);
}
static PyObject *
func_ret_struct(PyObject *self, PyObject *args) {
test_st_t *st;
// Получаем структуру из Python
if (!PyArg_ParseTuple(args, "O", &st)) // O - объект данных
Py_RETURN_NONE;
printf("C get test_st: val1 - %d, val2 - %f, val3 - %d\n", st->val1++, st->val2++, st->val3++);
return Py_BuildValue("O", st);
}
Модулю требуется указать что он в себя будет включать: функции, глобальные переменные и структуры. Каждую такую вещь нужно описать, самое сложное для своих типов данных(структуры...) Примерно такой файл генерирует cffi.
Для работы необходимо подключить заголовочные файлы:
#include <Python.h>
#include <structmember.h> // Для пользовательских типов данных
Флаги компиляции:
$(python3-config --includes --ldflags) -fPIC
За обработку аргументов отвечает следующая функция:
PyArg_ParseTuple(args, "ids", &val1, &val2, &val3);
1-ым идет аргумент типа int, он имеет литерное обозначение i
2-ым float/double — d
3-им string — s
Все возможные литерные обозначения типов данных можно посмотреть здесь
Теперь перейдем к описанию как описать структуру.
struct.c:
// Освобождение структуры
static void
test_st_t_dealloc(test_st_t* self) {
Py_TYPE(self)->tp_free((PyObject*)self);
}
// Создание структуры
static PyObject *
test_st_t_new(PyTypeObject *type, PyObject *args, PyObject *kwds) {
test_st_t *self;
self = (test_st_t *)type->tp_alloc(type, 0);
if (self != NULL) {
self->val1 = 0;
self->val2 = 0.0;
self->val3 = 0;
}
return (PyObject *)self;
}
// Инициализация структуры, заполняем её переданными значениями
static int
test_st_t_init(test_st_t *self, PyObject *args, PyObject *kwds) {
static char *kwlist[] = {"val1", "val2", "val3", NULL};
if (! PyArg_ParseTupleAndKeywords(args, kwds, "|idb", kwlist, &self->val1, &self->val2, &self->val3))
return -1;
return 0;
}
// Описываем аттрибуты из которых состоит структура
static PyMemberDef test_st_t_members[] = {
{"val1", T_INT, offsetof(test_st_t, val1), 0, "int"},
{"val2", T_DOUBLE, offsetof(test_st_t, val2), 0, "double"},
{"val3", T_CHAR, offsetof(test_st_t, val3), 0, "char"},
{NULL}
};
// Метод структуры, который печатает структуру
static PyObject* test_st_print(PyObject *self, PyObject *args)
{
test_st_t *st;
// Получаем структуру из Python
if (!PyArg_ParseTuple(args, "O", &st)) // O - объект данных
Py_RETURN_NONE;
printf("method: val1 - %d, val2 - %f, val3 - %d\n", st->val1++, st->val2++, st->val3++);
Py_RETURN_NONE;
}
// Описание методов стрктуры, но у классической структуры не может быть методов!
// А здесь может!
static PyMethodDef test_st_t_methods[] = {
{"print", test_st_print, METH_VARARGS, "doc string"},
{NULL} /* Sentinel */
};
// Структура описывающая нашу структуру. Какие атрибуты, методы, конструкторы, деструкторы и т.д. и т.п.
PyTypeObject test_st_t_Type = {
PyVarObject_HEAD_INIT(NULL, 0)
"_test.test_st_t", /* tp_name */
sizeof(test_st_t), /* tp_basicsize */
0, /* tp_itemsize */
(destructor) test_st_t_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
0, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
0, /* tp_hash */
0, /* tp_call */
0, /* tp_str */
0, /* tp_getattro */
0, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT |
Py_TPFLAGS_BASETYPE, /* tp_flags */
"test_st_t objects", /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
0, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
test_st_t_methods, /* tp_methods */
test_st_t_members, /* tp_members */
0, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
(initproc) test_st_t_init, /* tp_init */
0, /* tp_alloc */
test_st_t_new, /* tp_new */
};
И это все для:
struct test_st_s {
PyObject_HEAD // Макрос объявления нового типа, объекта фиксированного размера
int val1;
double val2;
char val3;
};
Согласитесь, не мало. Причем для структуры можно определять методы (в качестве примера test_st_print).
В коде стараюсь делать больше комментариев, что бы меньше описывать отдельно.
Python
Пример работы с C модулем из Python:
import sys
import time
# пути до модуля _test
sys.path.append('.')
sys.path.append('lib/')
sys.path.append('../../lib/')
import _test
###
## C
###
print("C API\n")
print("C\n")
start_time = time.time()
##
# Работа с функциями
##
print('Работа с функциями:')
print('ret func_hello: ', _test.func_hello())
print('ret func_ret_int: ', _test.func_ret_int(101))
print('ret func_ret_double: ', _test.func_ret_double(12.123456789))
print('ret func_ret_str: ', _test.func_ret_str('Hello!'))
print('ret func_many_args: ', _test.func_many_args(15, 18.1617, "Many arguments!"))
##
# Работа с переменными
##
print('\nРабота с переменными:')
print('ret a: ', _test.a)
# Изменяем значение переменной.
_test.a = 22
print('new a: ', _test.a)
print('ret b: ', _test.b)
print('ret c: ', _test.c)
##
# Работа со структурами
##
print('\nРабота со структурами:')
# Создаем структуру
st = _test.test_st_t(1, 2.3456789, 88)
print('st.val1 = {}\nst.val2 = {}\nst.val3 = {}'.format(st.val1, st.val2, st.val3))
st = _test.func_ret_struct(st)
print("ret func_ret_struct:")
print('st.val1 = {}\nst.val2 = {}\nst.val3 = {}'.format(st.val1, st.val2, st.val3))
# Вызывай метод print нашей структуры, только по скольку C частично ООП
# То нужно в этод метод передать указатель на нашу структуру
st.print(st)
# Время работы
print("--- {} seconds ---".format((time.time() - start_time)))
Модуль стал родным.
Плюсы и минусы C API
Плюсы:
- легко использовать в Python
Минусы:
- сложно описывать свои типы данных на C API
- сложно реализовать чистым Python программистам, да и не им тоже… (по мне самое простое через ctypes)
- модуль(библиотека) будет только для Python
Среднее время выполнения теста на каждом способе при 1000 запусках:
- ctypes: — 0.0004987692832946777 seconds ---
- CFFI: — 0.00038521790504455566 seconds ---
- pybind: — 0.0004547207355499268 seconds ---
- C API: — 0.0003561973571777344 seconds ---
Ссылки
- Исходные коды примеров
- Способ через CFFI, pybind11
- Способ через ctypes
- Python из C (C API)
Комментарии (8)
Gadd
27.09.2019 14:31+1Спасибо за статью.
На сколько я понимаю, основной плюс данного метода — это то, что можно ускорить узкие места написанного Python-кода, реализовав его части на C.
По сравнению с другими методами (например, по сравнению с ctypes) этот не имеет накладных расходов на преобразование данных и его можно применять для небольших, часто вызываемых функций, с бешовной интеграцией в Python код.
ctypes же лучше использовать для выполнения каких-то достаточно тяжёлых, относительно продолжительных вычислений, когда наладные расходы на преобразование не существенны. Основной плюс в том, что можно использовать существующую библиотеку.
И ещё, множество встроенных (и сторонних) функций и других объектов Python на самом деле написаны на С с использованием Python/C API, что делает их эффективными.
Простите за некоторое капитанство, давно интересуюсь темой, но начать разбираться самому не хватает духу и времени. Надеюсь, статья послужит толчком.
Если я в чем-то ошибаюсь — поправьте, пожалуйста.
P.S. часто использующие Python/C API для ускорения Python — поделитесь здесь своим опытом, пожалуйста.
idimus
27.09.2019 16:05Статьи про Cython и Boost.Python будут? На фоне этих технологий написание чистого C API выглядит абсолютно неразумной тратой времени и несут чисто академическую ценность, как мне кажется.
Jessy_James Автор
27.09.2019 16:36Пока не знаю, может если не лень будет через недельку другую займусь.
Этим вопросом занялся от нечего делать )
DollaR84
Полностью с вами согласен. Если python модуль и принимает функции библиотеки как родные, но сложность создания библиотеки, как по мне, намного превышает сложность написания python кода для ctypes.
И это тоже мне кажется важно. В случае ctypes, все нюансы связи пишутся в python модуле, что позволяет создать стандартную библиотеку, которую можно потом использовать в любых проектах, как на python, так и на C++, или любых других языках, позволяющих подключать внешние библиотеки.
Jessy_James Автор
Часа за 3 разобрался как функции и внешние переменные сделать, и еще часов 9 ушло что бы со структурой разобраться.
DollaR84
Ну осилить можно все при желании :)
Тут наверно стоит исходить из назначения библиотеки.
Нужна универсальность — пишем стандартную библиотеку под ctypes.
Нужна скорость — разбираемся с C API, так как скорость работы тут наверно самое показательное.