main

Продолжаем тему как вызывать C/C++ из Python3. Теперь используем библиотеки cffi, pybind11. Способ через ctypes был рассмотрен в предыдущей статье.


C


Тестовая библиотека для демонстрации работы с глобальными переменными, структурами и функциями с аргументами различных типов.
test.h


typedef struct test_st_s test_st_t;

extern int a;
extern double b;
extern char c;

int func_ret_int(int val);
double func_ret_double(double val);
char *func_ret_str(char *val);
char func_many_args(int val1, double val2, char val3, short val4);
test_st_t *func_ret_struct(test_st_t *test_st);

struct test_st_s {
    int val1;
    double val2;
    char val3;
};

test.c


#include <stdio.h>
#include <stdlib.h>
#include "test.h"

int a = 5;
double b = 5.12345;
char c = 'X';

int 
func_ret_int(int val) { 
    printf("C get func_ret_int: %d\n", val);
    return val;
} 

double 
func_ret_double(double val) { 
    printf("C get func_ret_double: %f\n", val);
    return val;
} 

char *
func_ret_str(char *val) { 
    printf("C get func_ret_str: %s\n", val);
    return val;
} 

char
func_many_args(int val1, double val2, char val3, short val4) { 
    printf("C get func_many_args: int - %d, double - %f, char - %c, short - %d\n", val1, val2, val3, val4);
    return val3;
} 

test_st_t *
func_ret_struct(test_st_t *test_st) {     
    if (test_st) {
        printf("C get test_st: val1 - %d, val2 - %f, val3 - %c\n", test_st->val1, test_st->val2, test_st->val3);
    }

    return test_st;
} 

Библиотека точно такая же как в статье про ctypes.


CFFI


Это библиотека для работы исключительно с C. Из описания этой библиотеки:


Interact with almost any C code from Python

Какую-то часть от этого почти удалось найти.


Для эксперимента использовалась версия 1.12.3, почитать про неё можно здесь.


Немного об этой библиотеки в 2-х словах, CFFI генерирует поверх нашей библиотеки свою обвязку и компилирует её в библиотеку с которой мы и будем работать.


Установка


pip3 install cffi


Сборка


Скрипт сборки, который будет собирать обвязку вокруг нашей библиотеки.


build.py


import os
import cffi

if __name__ == "__main__":
    ffi = cffi.FFI()
    # Путь расположение скрипта
    PATH = os.getcwd()

    # test.h заголовочный файл нашей библиотеки
    # указываем путь до него относительно build.py
    with open(os.path.join(PATH, "src/c/test.h")) as f:
        ffi.cdef(f.read())

    ffi.set_source("_test", # имя библиотеки собранной cffi, добавляем префикс _
        # Подключаем test.h, указываем путь относительно собираемой _test
        '#include "../src/c/test.h"',
        # Где находится libtest.so (Исходная собранная библиотека) 
        # относительно _test.cpython-36m-x86_64-linux-gnu.so (создается CFFI)
        libraries=[os.path.join(PATH, "lib/test"), "./test"],
        library_dirs=[PATH, 'objs/'],
    )

    # компилируем _test в папку lib
    ffi.compile(tmpdir='./lib')

Python


Пример работы с C из Python через CFFI:


from cffi import FFI
import sys
import time

# пути до модуля _test
sys.path.append('.')
sys.path.append('lib/')
sys.path.append('../../lib/')

# подключаем модуль
import _test

###
## C
###

print("CFFI\n")
print("C\n")

start_time = time.time()

##
# Работа с функциями
##

print('Работа с функциями:')
print('ret func_ret_int: ', _test.lib.func_ret_int(101))
print('ret func_ret_double: ', _test.lib.func_ret_double(12.123456789))
# Необходимо строку привести из cdata к массиву байтов, и массив байтов к строке.
print('ret func_ret_str: ', _test.ffi.string(_test.lib.func_ret_str('Hello!'.encode('utf-8'))).decode("utf-8"))
print('ret func_many_args: ', _test.lib.func_many_args(15, 18.1617, 'X'.encode('utf-8'), 32000).decode("utf-8"))

##
# Работа с переменными
##

print('\nРабота с переменными:')
print('ret a: ', _test.lib.a)

# Изменяем значение переменной.
_test.lib.a = 22
print('new a: ', _test.lib.a)

print('ret b: ', _test.lib.b)

print('ret c: ', _test.lib.c.decode("utf-8"))

##
# Работа со структурами
##

print('\nРабота со структурами:')

# Создаем структуру и заполняем её
test_st = _test.ffi.new("test_st_t *")
test_st.val1 = 5
test_st.val2 = 5.1234567
test_st.val3 = 'Z'.encode('utf-8')

ret = _test.lib.func_ret_struct(test_st)

# Полученные данные из C
print('ret val1 = {}\nret val2 = {}\nret val3 = {}'.format(ret.val1, ret.val2, ret.val3.decode("utf-8")))

# Время работы
print("--- %s seconds ---" % (time.time() - start_time))

Для работы с C++ кодом, нужно написать C обвязку для него. В статье про способ через ctypes описано как это сделать. Ссылка внизу.


Плюсы и минусы CFFI


Плюсы:


  • простой синтаксис при использовании в Python
  • не нужно перекомпилировать исходную библиотеку

Минусы:


  • не удобная сборка, нужно прописывать пути до всех заголовочных файлов и библиотек
  • создается еще 1-на динамическая библиотека, которая использует исходную
  • не поддерживает следующие директивы:
    #ifdef  __cplusplus
    extern "C" {
    #endif
    ...
    #ifdef  __cplusplus
    }
    #endif

    #ifndef _TEST_H_
    #define _TEST_H_
    ...
    #endif  /* _TEST_H_ */

pybind11


pybind11 напротив разработана специально для работы с C++. Для эксперимента использовалась версия 2.3.0, почитать про неё можно здесь. C исходники она не собирает, поэтому перевел их в C++ исходники.


Установка


pip3 install pybind11


Сборка


Нужно написать скрипт сборки нашей библиотеки.
build.py


import pybind11
from distutils.core import setup, Extension

ext_modules = [
    Extension(
        '_test', # имя библиотеки собранной pybind11
        ['src/c/test.cpp'], # Тестовый файлик который компилируем
        include_dirs=[pybind11.get_include()],  # не забываем добавить инклюды pybind11
        language='c++', # Указываем язык
        extra_compile_args=['-std=c++11'], # флаг с++11
    ),
]

setup(
    name='_test', # имя библиотеки собранной pybind11
    version='1.0.0',
    author='djvu',
    author_email='djvu@inbox.ru',
    description='pybind11 extension',
    ext_modules=ext_modules,
    requires=['pybind11'],  # Указываем зависимость от pybind11
    package_dir = {'': 'lib'}
)

Выполняем его:


python3 setup.py build --build-lib=./lib

C++


В исходники библиотеки нужно добавить:


  • заголовочный файл pybind11
    #include <pybind11/pybind11.h>
  • макрос, который позволяет нам определить Python модуль
    PYBIND11_MODULE(_test, m)
  • пример макроса для тестовой библиотеки:

namespace py = pybind11;

// _test имя нашего модуля
PYBIND11_MODULE(_test, m) {
    /*
     * Функции библиотеки
     */

    m.def("func_ret_int", &func_ret_int);
    m.def("func_ret_double", &func_ret_double);
    m.def("func_ret_str", &func_ret_str);
    m.def("func_many_args", &func_many_args);
    m.def("func_ret_struct", &func_ret_struct);

    /*
     * Глобальные переменные библиотеки
     */

    m.attr("a") = a;
    m.attr("b") = b;
    m.attr("c") = c;

    /*
     * Структуры
     */

    py::class_<test_st_t>(m, "test_st_t")
    .def(py::init()) // Указываем конструктор. Которого у структуры нет, но он нужен Python
    // Но поскольку у нас теперь не C, а C++ то есть(меня как разработчика C бесит что структура в ++ это публичный класс)
    .def_readwrite("val1", &test_st_t::val1) // переменные структуры
    .def_readwrite("val2", &test_st_t::val2)
    .def_readwrite("val3", &test_st_t::val3); 
};

Python


Пример работы с C из Python через pybind11:


import sys
import time

# Пути до модуля _test
sys.path.append('lib/')

# подключаем модуль
import _test

###
## C
###

print("pybind11\n")
print("C\n")

start_time = time.time()

##
# Работа с функциями
##

print('Работа с функциями:')
print('ret func_ret_int: ', _test.func_ret_int(101))
print('ret func_ret_double: ', _test.func_ret_double(12.123456789))
# Необходимо строку привести из cdata к массиву байтов.
print('ret func_ret_str: ', _test.func_ret_str('Hello!'.encode('utf-8')))
print('ret func_many_args: ', _test.func_many_args(15, 18.1617, 'X'.encode('utf-8'), 32000))

##
# Работа с переменными
##

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Работа со структурами:')

# Создаем структуру и заполняем её
_test_st = _test.test_st_t()
#print(dir(_test_st))
_test_st.val1 = 5
_test_st.val2 = 5.1234567
_test_st.val3 = 'Z'.encode('utf-8')

ret = _test.func_ret_struct(_test_st)

# Полученные данные из C
print('ret val1 = {}\nret val2 = {}\nret val3 = {}'.format(ret.val1, ret.val2, ret.val3))

# Время работы
print("--- %s seconds ---" % (time.time() - start_time))

Плюсы и минусы pybind11


Плюсы:


  • простой синтаксис при использовании в Python

Минусы:


  • необходимо править C++ исходники, или писать обвязку для них
  • необходимо собирать нужную библиотеку из исходников

Среднее время выполнения теста на каждом способе при 1000 запусках:


  • ctypes: — 0.0004987692832946777 seconds ---
  • CFFI: — 0.00038521790504455566 seconds ---
  • pybind: — 0.0004547207355499268 seconds ---

+,- т.к результаты каждый раз немного отличались. Плюс тратилось время на печать, которую мне было лень отключить(возьмем это время за константу т.к. она во всех тестах будет ~ одинаковая). Но все таки разница по времени в вызовах функции и получения результатов из них есть.


Ссылки


Комментарии (12)


  1. DollaR84
    19.09.2019 21:39

    Я может где-то не заметил, но в примерах вроде есть строки для замера времени выполнения, но не заметил сравнения cffi, pybind11 и ctypes. Интересно сравнить время исполнения, так как реализация мне кажется у них усложнена, по сравнению с ctypes.


    1. Jessy_James Автор
      19.09.2019 23:48

      Да, заложился что бы замерить и забыл про это. Завтра прогоню и результаты здесь выложу.


    1. Jessy_James Автор
      20.09.2019 12:09
      +1

      Выложил результаты в конце статьи, как ни странно ctypes оказался медленнее. Что меня удивило…


      1. DollaR84
        20.09.2019 12:58

        Да, удивительно… Спасибо. С pybind11 вроде разница не особо, а вот cffi значительно… Надо будет покопать и посмотреть что да как с ним более тщательно. Напрягли меня целый ряд ограничений, указанных вами для cffi. Ладно, спасибо за статью, и результаты сравнения.


        1. Jessy_James Автор
          20.09.2019 15:54
          +1

          cffi.CDefError: cannot parse "#ifndef _TEST_H_"
          cdef source string:1:1: Directives not supported yet

          Может быть разработчик в следующих версиях добавит поддержку…


  1. Gadd
    20.09.2019 10:54

    Будет статья про Python/C API?


    1. Jessy_James Автор
      20.09.2019 10:55

      1. Gadd
        20.09.2019 10:59

        Спасибо. Как-то эта статья мимо меня прошла
        UPD: нет, всё-таки не прошла. В той статье про вызов Python кода из С. Я имел в виду С-расширения для Python с использованием Python/C API (не путать с этой статьёй). Похоже, конечно же, но всё-таки немного с другой стороны.


        1. Jessy_James Автор
          20.09.2019 12:14

          Поинтересуюсь этим попозже.


  1. Jessy_James Автор
    20.09.2019 10:54

    Не туда написал, не могу удалить комент.


  1. menstenebris
    20.09.2019 13:35

    На python проверять время исполнения через time.time() не очень корректно, особенно для маленьких операций. Есть встроенная библиотека timeit, разработанная специально для проверок производительности.


    1. Jessy_James Автор
      20.09.2019 15:19

      Ок, погляжу и тесты не поленюсь нормальнее напишу.