main

Заключительная статья из серии как вызывать C/C++ из Python3, перебрал все известные способы как можно это сделать. На этот раз добрался до boost. Что из этого вышло читаем ниже.


C


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


test.c:


#include "test.hpp"

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;
}

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

    return object(string(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;
}

// _test имя нашего модуля
BOOST_PYTHON_MODULE(_test) {

    /*
     * Функции библиотеки
     */

    def("func_ret_int", func_ret_int);
    def("func_ret_double", func_ret_double);
    def("func_ret_str", &func_ret_str);
    def("func_many_args", func_many_args);

    // Очень важно
    // manage_new_object C функция возвращает новый объект
    // reference_existing_object C функция возвращает существующий объект
    def("func_ret_struct", &func_ret_struct, return_value_policy<reference_existing_object>());

    /*
     * Глобальные переменные библиотеки
     */
    scope().attr("a") = a;
    scope().attr("b") = b;
    scope().attr("c") = c;

    /*
     * Структуры
     */
    class_<test_st_t>("test_st_t")
        .def_readwrite("val1", &test_st_t::val1)
        .def_readwrite("val2", &test_st_t::val2)
        .def_readwrite("val3", &test_st_t::val3)
    ;

}

test.h:


using namespace boost::python;
using namespace std;

#ifdef  __cplusplus
extern "C" {
#endif

typedef struct test_st_s test_st_t;
typedef char * char_p;

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

int func_ret_int(int val);
double func_ret_double(double val);

object 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;
};

#ifdef  __cplusplus
}
#endif

Как компилировать :


g++ -g -fPIC -I/usr/include/python3.6 -I./src/c  -o ./objs/test.o -c ./src/c/test.cpp
g++ -fPIC -g -shared -o ./lib/_test.so ./objs/test.o  -lboost_python3

Исходник компилируется в динамическую библиотеку.
python boost похож в использовании на pybind11, так же нужно описать функции которые будут видны python. Но по моим ощущения boost более громоздкий и сложный. Например:


def("func_ret_struct", &func_ret_struct, return_value_policy<reference_existing_object>());

Функция func_ret_struct принимает в качестве аргумента указатель на структуру и возвращает этот же указатель назад. Для нее нужно указать правила возвращаемого объекта return_value_policy<reference_existing_object>(). reference_existing_objec говорит, что возвращаемый объект уже существовал. Если указать manage_new_object, то это будет значить, что мы возвращаем новый объект. В таком случае вот такой скрипт упадет в segmentation fault на сборщике мусора:


test_st = _test.test_st_t()

ret = _test.func_ret_struct(test_st)

Потому что сборщик мусора сначала очистит данные которые содержит test_st, а потом захочет очистить данные которые содержит объект ret. Который содержит те же самые данные, что содержал test_st, но они уже были очищены.


Интересно как в таком случае описать вот такую функцию(не стал углубляться)?:


test_st_t *
func_ret_struct(test_st_t *test_st) {
    if (test_st) {
        return test_st;
    } else {
        return (test_st_t *) malloc(sizeof(test_st_t)); 
    }
}

Такая функция может вернуть как уже существующий объект, так и существовавший.


Так же у меня возникла проблема с такой функцией:


char *
func_ret_str(char *val) {
    return val;
}

Как я понял, получить из boost указатель в python на стандартный тип данных нельзя. Можно только на struct, class и union. Если кто знает способ просветите.


Python


Для python модуль становится родным.
main.py:


#!/usr/bin/python3
#-*- coding: utf-8 -*-

import sys
import time

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

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

###
## C
###

print("boost\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))
print('ret func_ret_str: ', _test.func_ret_str('Hello!'))
print('ret func_many_args: ', _test.func_many_args(15, 18.1617, 'X', 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()
test_st.val1 = 5
test_st.val2 = 5.1234567
test_st.val3 = 'Z'

print('val1 = {}\nval2 = {}\nval3 = {}'.format(test_st.val1, test_st.val2, test_st.val3))

ret = _test.func_ret_struct(test_st)

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

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

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


Плюсы:


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

Минусы:


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

Код как обычно стараюсь комментировать понятно.


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


  • ctypes: — 0.0004987692832946777 seconds ---
  • CFFI: — 0.00038521790504455566 seconds ---
  • pybind: — 0.0004547207355499268 seconds ---
  • C API: — 0.0003561973571777344 seconds ---
  • boost: — 0.00037789344787597656 seconds ---

Ссылки


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


  1. gudvinr
    15.10.2019 18:55

    И вы действительно время точнее одной аттосекунды получали?
    Насколько мне известно, таймеры ОС не умеют выдавать результаты точнее 10^-9


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


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


    У вас ведь все данные на руках есть, чтобы получить значения, которые будут прекрасно иллюстрировать результат.


    1. Jessy_James Автор
      15.10.2019 19:03

      И вы действительно время точнее одной аттосекунды получали?

      Нет, конечно. Я использую ctrl-c ctrl-v, примерный результат по времени дает, что вполне сойдет. Заниматься максимальным точным сравнением времени выполнения не буду.


      1. gudvinr
        15.10.2019 19:31

        примерный результат по времени дает, что вполне сойдет

        Те значения, которые вы приводите, никакого результата не дают. У вас значения находятся от 357мкс в минимальном случае до 499мкс в максимальном.


        Если разброс при этом составляет хотя бы 70мкс, что может быть объяснимо даже обычными задержками I/O, то в плане скорости все способы одинаковы. Но, так как никакой информации об этом нет, то выводы на основе этих чисел делать нельзя.


        Если уж вам не лень было подготовить материал, на который вы потратили наверняка не один час, то минутку-другую можно же уделить тому, чтобы причесать пару чисел.


        timeit, к примеру, позволяет вывести все результаты, по которым в excel/google docs можно одной функцией получить среднее.
        Ноаже если указать результат с точностью до наименьших значащих чисел (десятки микросекунд в вашем случае) и поместить в табличку, это сильно улучшает наглядность.


        1. Jessy_James Автор
          15.10.2019 19:34

          про timeit мне кто то уже здесь писал, возможно переборю лень ), и сделаю с ним, а потом здесь обновлю показания.


  1. neurocod
    15.10.2019 20:47

    swig смотрели? Хотя он использует и свой метод, но для практики весьма удобно.


    1. Jessy_James Автор
      16.10.2019 01:08

      Нет, про это не слышал.


  1. MSC6502
    15.10.2019 23:00

    -Мы рисковали, и должны получить какую-то компенсацию.
    -А, так всё таки вы торговцы.


    Без Си всё таки никуда, каким бы «новым»и «мощным» не был очередной ЯП.
    Зорг и тут прав.