main

В прошлом году появилась необходимость дополнить старый проект написанный на C функционалом на python3. Не смотря на то, что есть статьи на эту тему я помучился и в том году и сейчас когда писал программы для статьи. Поэтому приведу свои примеры по тому как работать с python3 из C под Linux (с тем что использовал). Опишу как создать класс и вызвать его методы, получить доступ к переменным. Вызов функций и получение переменных из модуля. А также проблемы с которыми я столкнулся и не смог их понять.


Пакеты


Используем стандартное Python API. Необходимые пакеты python:


  • python3
  • python3-dev
  • python3-all
  • python3-all-dev
  • libpython3-all-dev

Работа в интерпретаторе


Самое простое, загрузка интерпретатора python и работа в нем.


Для работы необходимо подключить заголовочный файл:


 #include <Python.h>

Загружаем интерпретатор:


Py_Initialize();

Далее идет блок работы с python, например:


PyRun_SimpleString("print('Hello!')");

Выгружаем интерпретатор:


Py_Finalize();

Полный пример:


#include <Python.h>

void
main() {
    // Загрузка интерпретатора Python
    Py_Initialize();
    // Выполнение команды в интерпретаторе
    PyRun_SimpleString("print('Hello!')");
    // Выгрузка интерпретатора Python
    Py_Finalize();
}

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


gcc simple.c $(python3-config --includes --ldflags) -o simple && ./simple
Hello!

А вот так не будет работать:


gcc $(python3-config --includes --ldflags)  simple.c -o simple && ./simple
/tmp/ccUkmq57.o: In function `main':
simple.c:(.text+0x5): undefined reference to `Py_Initialize'
simple.c:(.text+0x16): undefined reference to `PyRun_SimpleStringFlags'
simple.c:(.text+0x1b): undefined reference to `Py_Finalize'
collect2: error: ld returned 1 exit status

Все из-за того, что python3-config --includes --ldflags раскрывается вот в такую штуку:


-I/usr/include/python3.6m -I/usr/include/python3.6m
-L/usr/lib/python3.6/config-3.6m-x86_64-linux-gnu -L/usr/lib -lpython3.6m -lpthread -ldl  -lutil -lm  -Xlinker -export-dynamic -Wl,-O1 -Wl,-Bsymbolic-functions

Здесь думаю важен порядок подключения линкера -Wl. Кто знает точнее напишите про это в коментах, дополню ответ.


Объяснение от MooNDeaR:


Всё довольно просто — символы ищутся в один проход и все неиспользуемые выбрасываются. Если поставить simple.c в конец, то получается, что использование символа Py_Initialize() линкер увидит после того, как посмотрит в библиотеки питона, все символы которых будут к этому моменту выброшены (потому что не использовались).

Пример вызова функции из файла python:
simple.c


#include <Python.h>

void 
python() {
    // Загрузка интерпретатора Python
    Py_Initialize();

    // Выполнение команд в интерпретаторе
    // Загрузка модуля sys
    PyRun_SimpleString("import sys");
    // Подключаем наши исходники python
    PyRun_SimpleString("sys.path.append('./src/python')");
    PyRun_SimpleString("import simple");
    PyRun_SimpleString("print(simple.get_value(2))");
    PyRun_SimpleString("print(simple.get_value(2.0))");
    PyRun_SimpleString("print(simple.get_value(\"Hello!\"))");

    // Выгрузка интерпретатора Python
    Py_Finalize();  
}

void
main() {
    puts("Test simple:");

    python();
}

simple.py


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

def get_value(x):
    return x

Но это простые и неинтересные вещи, мы не получаем результат выполнения фунции.


Работа с функциями и переменными модуля


Здесь немного сложнее.
Загрузка интерпретатора python и модуля func.py в него:


PyObject *
python_init() {
    // Инициализировать интерпретатор Python
    Py_Initialize();

    do {
        // Загрузка модуля sys
        sys = PyImport_ImportModule("sys");
        sys_path = PyObject_GetAttrString(sys, "path");
        // Путь до наших исходников python
        folder_path = PyUnicode_FromString((const char*) "./src/python");
        PyList_Append(sys_path, folder_path);

        // Загрузка func.py
        pName = PyUnicode_FromString("func");
        if (!pName) {
            break;
        }

        // Загрузить объект модуля
        pModule = PyImport_Import(pName);
        if (!pModule) {
            break;
        }

        // Словарь объектов содержащихся в модуле
        pDict = PyModule_GetDict(pModule);
        if (!pDict) {
            break;
        }

        return pDict;
    } while (0);

    // Печать ошибки
    PyErr_Print();
}

Освобождение ресурсов интерпретатора python:


void
python_clear() {
    // Вернуть ресурсы системе
    Py_XDECREF(pDict);
    Py_XDECREF(pModule);
    Py_XDECREF(pName);

    Py_XDECREF(folder_path);
    Py_XDECREF(sys_path);
    Py_XDECREF(sys);

    // Выгрузка интерпретатора Python
    Py_Finalize();
}

Работа с переменными и функциями модуля.


/**
 * Передача строки в качестве аргумента и получение строки назад
 */
char *
python_func_get_str(char *val) {
    char *ret = NULL;

    // Загрузка объекта get_value из func.py
    pObjct = PyDict_GetItemString(pDict, (const char *) "get_value");
    if (!pObjct) {
        return ret;
    }

    do {
        // Проверка pObjct на годность.
        if (!PyCallable_Check(pObjct)) {
            break;
        }

        pVal = PyObject_CallFunction(pObjct, (char *) "(s)", val);
        if (pVal != NULL) {
            PyObject* pResultRepr = PyObject_Repr(pVal);

            // Если полученную строку не скопировать, то после очистки ресурсов python её не будет.
            // Для начала pResultRepr нужно привести к массиву байтов.
            ret = strdup(PyBytes_AS_STRING(PyUnicode_AsEncodedString(pResultRepr, "utf-8", "ERROR")));

            Py_XDECREF(pResultRepr);
            Py_XDECREF(pVal);
        } else {
            PyErr_Print();
        }
    } while (0);

    return ret;
}

/**
 * Получение значения переменной содержащей значение типа int
 */
int
python_func_get_val(char *val) {
    int ret = 0;

    // Получить объект с именем val
    pVal = PyDict_GetItemString(pDict, (const char *) val);
    if (!pVal) {
        return ret;
    }

    // Проверка переменной на long
    if (PyLong_Check(pVal)) {
        ret = _PyLong_AsInt(pVal);
    } else {
        PyErr_Print();
    }

    return ret;
}

На этом остановимся подробнее


pVal = PyObject_CallFunction(pObjct, (char *) "(s)", val);

"(s)" означает, что передается 1 параметр типа char * в качестве аргумента функции get_value(x). Если бы нам нужно будет передать несколько аргументов функции, то будет так:


pVal = PyObject_CallFunction(pObjct, (char *) "(sss)", val1, val2, val3);

Если необходимо передать int, то использовалась бы литера i, все возможные типы данных и их обозначения можно посмотреть в документации python.


pVal = PyObject_CallFunction(pObjct, (char *) "(i)", my_int);

func.py:


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

a = 11
b = 22
c = 33

def get_value(x):
    return x

def get_bool(self, x):
    if x:
        return True
    else:
        return False

(Проблема решена)
Проблема с которой я столкнулся и не смог пока понять:


int
main() {
    puts("Test func:");

    if (!python_init()) {
        puts("python_init error");
        return -1;
    }

    puts("Strings:");
    printf("\tString: %s\n", python_func_get_str("Hello from Python!"));

    puts("Attrs:");
    printf("\ta: %d\n", python_func_get_val("a"));
    printf("\tb: %d\n", python_func_get_val("b"));
    printf("\tc: %d\n", python_func_get_val("c"));

    python_clear();

    return 0;
}

Если хочу получить b или c из func.py, то на:


Py_Finalize();

получаю segmentation fault. С получением только a такой проблемы нет.
При получении переменных класса, тоже проблем нет.


Объяснение от pwl:


PyObject PyDict_GetItemString(PyObject p, const char *key)
Return value: Borrowed reference. Nothing needs to be done for a borrowed reference.

Проблема была связана с тем, что я вызывал Py_XDECREF() для PyDict_GetItemString(). Так делать для этой функции ненужно, приводит к segmentation fault.


Работа с классом


Тут еще немножечко посложнее.
Загрузка интерпретатора python и модуля class.py в него.


PyObject *
python_init() {
    // Инициализировать интерпретатор Python
    Py_Initialize();

    do {
        // Загрузка модуля sys
        sys = PyImport_ImportModule("sys");
        sys_path = PyObject_GetAttrString(sys, "path");
        // Путь до наших исходников python
        folder_path = PyUnicode_FromString((const char*) "./src/python");
        PyList_Append(sys_path, folder_path);

        // Создание Unicode объекта из UTF-8 строки
        pName = PyUnicode_FromString("class");
        if (!pName) {
            break;
        }

        // Загрузить модуль class
        pModule = PyImport_Import(pName);
        if (!pModule) {
            break;
        }

        // Словарь объектов содержащихся в модуле
        pDict = PyModule_GetDict(pModule);
        if (!pDict) {
            break;
        }

        // Загрузка объекта Class из class.py
        pClass = PyDict_GetItemString(pDict, (const char *) "Class");
        if (!pClass) {
            break;
        }

        // Проверка pClass на годность.
        if (!PyCallable_Check(pClass)) {
            break;
        }

        // Указатель на Class
        pInstance = PyObject_CallObject(pClass, NULL);

        return pInstance;
    } while (0);

    // Печать ошибки
    PyErr_Print();
}

Передача строки в качестве аргумента и получение строки назад


char *
python_class_get_str(char *val) {
    char *ret = NULL;

    pVal = PyObject_CallMethod(pInstance, (char *) "get_value", (char *) "(s)", val);
    if (pVal != NULL) {
        PyObject* pResultRepr = PyObject_Repr(pVal);

        // Если полученную строку не скопировать, то после очистки ресурсов python её не будет.
        ret = strdup(PyBytes_AS_STRING(PyUnicode_AsEncodedString(pResultRepr, "utf-8", "ERROR")));

        Py_XDECREF(pResultRepr);
        Py_XDECREF(pVal);
    } else {
        PyErr_Print();
    }

    return ret;
}

Здесь ни каких проблем не было, все работает без ошибок. В исходниках примеры, как работать с int, double, bool.


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


Ссылки


Исходные коды примеров
Следующая статья C/C++ из Python

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


  1. alec_kalinin
    04.09.2019 15:40
    +2

    Спасибо, полезная статья!

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


    1. Mixaill
      04.09.2019 16:25
      +1

      Как альтернативу можно рассмотреть pybind11. Почти то же самое, только Boost тащить не нужно.


  1. MooNDeaR
    04.09.2019 17:40

    Здесь думаю важен порядок подключения линкера -Wl. Кто знает точнее напишите про это в коментах, дополню ответ.

    Всё довольно просто — символы ищутся в один проход и все неиспользуемые выбрасываются. Если поставить simple.c в конец, то получается, что использование символа Py_Initialize() линкер увидит после того, как посмотрит в библиотеки питона, все символы которых будут к этому моменту выброшены (потому что не использовались).


    В общем, порядок передачи аргументов важен. Есть способ решить эту проблему через флаги -Wl,--start-group и -Wl,--eng-group, но в данном случае это излишне.


    1. Jessy_James Автор
      04.09.2019 21:38

      Спасибо.


    1. 0xd34df00d
      04.09.2019 23:24

      И именно поэтому в особо запущенных случаях бывает необходимо одну и ту же библиотеку указывать несколько раз.


      1. pwl
        04.09.2019 23:58

        В особо запущенных это не поможет, т.к. особо запущенные — это циклические ссылки.
        Для борьбы с этом достаточно в самом начале указать -Wl,--start-group
        А end-group можно не указывать.


        1. 0xd34df00d
          05.09.2019 18:39

          Знаком с системами (кажется, это был то ли AIX, то ли SunOS), где этого нет.


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


  1. pwl
    04.09.2019 20:57

    Проблема с которой я столкнулся и не смог пока понять


    pVal = PyDict_GetItemString(pDict, (const char *) val);
    ...
    Py_XDECREF(pVal);
    


    PyObject* PyDict_GetItemString(PyObject *p, const char *key)
    Return value: Borrowed reference.

    When a function passes ownership of a reference on to its caller, the caller is said to receive a new reference. When no ownership is transferred, the caller is said to borrow the reference. Nothing needs to be done for a borrowed reference.


    1. Jessy_James Автор
      04.09.2019 21:39

      Спасибо.


  1. ybqwer
    05.09.2019 00:17

    Это очень странно — почему не из питона вызывать написанное на С а наоборот?


    1. pvvv
      05.09.2019 13:01
      +1

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

      Но всё-таки это взаимодействие туда/сюда в той же в Lua, через стэк, сделано гораздо проще и красивее.


      1. ybqwer
        05.09.2019 13:32
        -4

        Так обратно тоже надо,

        слона обучить кататься на роликах тоже надо. Питон это высокоуровневый язык удалённый от железа, С — низкоуровневый, близкий к железу. Кто кого должен вызывать? Принцип иерархии забыли?


        1. pvvv
          05.09.2019 13:55
          +1

          Может не самый удачный пример, но вот какой-нибудь CAD (freecad?), где пользователю дан питон для автоматизации своих действий и написания плагинов.
          Всё-таки наверное можно позволить пользователю набрать в консоли a=1; b=a+2; а потом из низкоуровнего C забрать значение 'b' у высокоуровневого питона и что-то с ним дальше делать?
          Или исходя из вашего принципа иерархии для этого его надо сначала целиком на высокоуровневом питоне переписать, а в качестве скриптового языка расширения наоборот прикрутить tcc какой-нибудь, раз он уровнем пониже?


          1. ybqwer
            05.09.2019 14:30
            -3

            а потом из низкоуровнего C забрать значение 'b' у высокоуровневого питона и что-то с ним дальше делать?

            пишем на С что мы со значением дальше делаем и используем это как функцию в более высокоуровневом ( в Питоне считываем с консоли и вызываем функцию С). Тот же принцип софтваредизайна например что низкоуровневая функция не должна вызывать высокоуровневую иначе будет спагетти — код. Тот же принцип что надо избегать GOTO выходы из цикла. Если рассматривать задачу слишком локально (как в твоём примере), то может показаться что удобно сделать так, но это приведёт потом к каше.
            А тут ещё плюсы таким постам ставят


            1. Jessy_James Автор
              05.09.2019 15:14

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

              GOTO, break, continue — это jump asm.


            1. pvvv
              05.09.2019 16:51

              Как это будет выглядеть на примере того же freeCADa?
              надо переписать его на высокоуровнем питоне, ну чтобы иерархия не нарушалась?
              Вот уж где каша будет.
              Хватает случаев когда скриптовый язык нужен лишь в качестве, грубо говоря, достаточно гибкого .ini файла, каким бы высокоуровневым он не был.


    1. Jessy_James Автор
      05.09.2019 14:47

      Так я тоже делал(позже может напишу про это).
      Мне было нужно для следующего. Был готовый проект(давно написанный) по обработке данных (парсинг текста и складирование в БД), а поддерживать его надо и появлялись новые форматы данных(которые нужно обрабатывать). И в конце, концов работать со строками в Си надоело, проще их отдать python и получить обработанные данные назад.


      1. ybqwer
        05.09.2019 15:42

        может тогда через REST, не прикручивая чёрт знает что в одной программе. А вообще задобал тут неграмотный народ.


        1. Jessy_James Автор
          05.09.2019 15:46

          Я давно все на python переписал )), тогда мне нужно было python хоть как-то пропихнуть для использования. У нас имелись некоторые ограничения на используемые языки.


          1. ybqwer
            05.09.2019 15:53

            а, нуда, у тебя

            парсинг текста и складирование в БД
            что есть высокоуровневая задача и С там вообще никаким боком не нужен. А можно подетальнее что и как там делалось? в наше цивилизованное время пора задуматься о распаралеливании париснга и прочих операций с текстом.


            1. Jessy_James Автор
              05.09.2019 16:05

              У нас используется postgresql, но для него написана своя библиотека которая делает работу с ним объектно ориентированной. На тот момент такой библиотеки для python еще не было(сейчас уже написали). Данные приходят в виде псевдотаблиц(столбцы, заголовки, разделители записей — разгораживаются символами |=-_+ и т.п.). Нужно вытаскивать значения из ячеек и писать в БД их.

              +-----------+-----------+
              |Cell1___|Cell2____|
              +-----------+-----------+
              |Val1____|Val2____|
              +-----------+-----------+

              Примерно так.

              Есть планировщик который такие таблицы раскидывает по воркерам. Но что бы одну таблицу распарсить нескольких воркерами это будет еще тот геморой )


              1. ybqwer
                05.09.2019 19:57

                . Данные приходят в виде псевдотаблиц
                откуда приходят, почему в таком виде? Вроде бы в общепринятых форматах типа JSON или YAML прямое отображение в ОО представление и в БД, зачем какой то левый формат юзать


                1. Jessy_James Автор
                  06.09.2019 00:20

                  Откуда приходят не скажу.
                  Вся эта система не мной придумана и не мной отменена будет. Так исторически сложилось годов с 80-х(про json не слышали к сожалению в те года), если не раньше. Каждая таблица имеет несколько страниц, страница размером 80 символов в ширину и 22 строки в высоту. Что бы целиком на старых экранах отображаться в dos или что там тогда было.
                  К тому же в те года каналы связи были очень чувствительны к размеру передаваемых данных => придумали гениальное решение все буквы кириллицы которые имеют сходные буквы в латинице заменять на эквиваленты. А (2 байта) ~ A (1 байт), К (2 байта) ~ K (1 байт)

                  Я то как раз это в json перегоняю.