C/C++ из Python (boost)

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

    Ссылки


    • +20
    • 4,3k
    • 7
    Поделиться публикацией

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

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

      0

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


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


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


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

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

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

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


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


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


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

            –2
            про timeit мне кто то уже здесь писал, возможно переборю лень ), и сделаю с ним, а потом здесь обновлю показания.
        +1
        swig смотрели? Хотя он использует и свой метод, но для практики весьма удобно.
          0

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

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


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

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

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