C/C++ из Python (C API)
- пятница, 27 сентября 2019 г. в 00:28:53
 

Продолжаем тему как вызывать C/C++ из Python3. Теперь используем C API для создания модуля, на этом примере мы сможем разобраться как работает cffi и прочие библиотеки упрощающие нам жизнь. Потому что на мой взгляд это самый трудный способ.
Тестовая библиотека для демонстрации работы с глобальными переменными, структурами и функциями с аргументами различных типов. В своих статьях использую вариации одной и той же библиотеки, в зависимости от способа которым я теперь пользуюсь. Ссылки на предыдущие способы внизу.
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Теперь перейдем к описанию как описать структуру.
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).
В коде стараюсь делать больше комментариев, что бы меньше описывать отдельно.
Пример работы с 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)))Модуль стал родным.
Плюсы:
Минусы: