C/C++ из Python (C API)

  • Tutorial
main

Продолжаем тему как вызывать 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 ---

Ссылки


Комментарии 8

    +1
    • сложно реализовать чистым Python программистам, да и не им тоже… (по мне самое простое через ctypes)

    Полностью с вами согласен. Если python модуль и принимает функции библиотеки как родные, но сложность создания библиотеки, как по мне, намного превышает сложность написания python кода для ctypes.
    • модуль(библиотека) будет только для Python

    И это тоже мне кажется важно. В случае ctypes, все нюансы связи пишутся в python модуле, что позволяет создать стандартную библиотеку, которую можно потом использовать в любых проектах, как на python, так и на C++, или любых других языках, позволяющих подключать внешние библиотеки.
      +1
      Часа за 3 разобрался как функции и внешние переменные сделать, и еще часов 9 ушло что бы со структурой разобраться.
        +1
        Ну осилить можно все при желании :)
        Тут наверно стоит исходить из назначения библиотеки.
        Нужна универсальность — пишем стандартную библиотеку под ctypes.
        Нужна скорость — разбираемся с C API, так как скорость работы тут наверно самое показательное.
      +3
      Спасибо за статью.
      На сколько я понимаю, основной плюс данного метода — это то, что можно ускорить узкие места написанного Python-кода, реализовав его части на C.
      По сравнению с другими методами (например, по сравнению с ctypes) этот не имеет накладных расходов на преобразование данных и его можно применять для небольших, часто вызываемых функций, с бешовной интеграцией в Python код.
      ctypes же лучше использовать для выполнения каких-то достаточно тяжёлых, относительно продолжительных вычислений, когда наладные расходы на преобразование не существенны. Основной плюс в том, что можно использовать существующую библиотеку.
      И ещё, множество встроенных (и сторонних) функций и других объектов Python на самом деле написаны на С с использованием Python/C API, что делает их эффективными.

      Простите за некоторое капитанство, давно интересуюсь темой, но начать разбираться самому не хватает духу и времени. Надеюсь, статья послужит толчком.

      Если я в чем-то ошибаюсь — поправьте, пожалуйста.

      P.S. часто использующие Python/C API для ускорения Python — поделитесь здесь своим опытом, пожалуйста.
      0
      Статьи про Cython и Boost.Python будут? На фоне этих технологий написание чистого C API выглядит абсолютно неразумной тратой времени и несут чисто академическую ценность, как мне кажется.
        +1
        Пока не знаю, может если не лень будет через недельку другую займусь.
        Этим вопросом занялся от нечего делать )
        0

        Вчера открыл для себя PyType_FromSpec.

        Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

        Самое читаемое