Skip to content

Latest commit

 

History

History
 
 

python

Folders and files

NameName
Last commit message
Last commit date

parent directory

..
 
 

Python: расширение и внедрение

Основной источник (на английском): Extending and Embedding.

Справочная информация (на английском): Python/C API.

Ипользование интерпретатора Python

Интерпретатор Python реализован в разделяемой библиотеке, а исполняемый файл интерпретатора python3 является лишь оболочкой для запуска интерпретатора.

Для сборки программы можно использовать CMake, в стандартной поставке которого входит поддержка Python.

find_package(PythonLibs 3.6 REQUIRED)
include_directories(${PYTHON_INCLUDE_DIRS})
target_link_libraries(program ${PYTHON_LIBRARIES})

Обратите внимание на то, что необходимо указывать минимальную версию интерпретатора, поскольку в поставку многих дистрибутивах Linux входит две версии Python (2.7 и 3.x), и может возникнуть неоднозначность используемой библиотеки.

Тривиальная реализация своего интерпретатора, с использованием библиотеки python выглядит следующим образом:

#include <stdio.h>
// В этом заголовочном файле собран почти весь API Python
#include <Python.h>

int main(int argc, char *argv[])
{
    // Открытие файла на чтение
    FILE* fp = fopen(argv[1], "r");
    // Инициализация интерпретатора Python
    Py_Initialize();
    // Выполнение файла
    PyRun_SimpleFile(fp, argv[1]);
    // Завершение работы интерпретатора
    Py_Finalize();
    // Закрытие файла
    fclose(fp);
}

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

/* Си */
PyRun_SimpleFile(fp, "abrakadabra");

# Python
print(__file__)
# abrakadabra

Скрипт на языке Python может иметь аргументы командной строки, которые доступны через переменную-список sys.argv. В приведенной выше тривиальной реализации интерпретатора это значение не установлено, поэтому обращение к sys.argv выдаст ошибку о том, что эта переменная не определена:

AttributeError: module 'sys' has no attribute 'argv'

Перед запуском файла на выполнение можно установить список аргументов с помощью PySys_SetArgv:

wchar_t* args[] = { L"One", L"Two", L"Аргумент" };
PySys_SetArgv(3, args); // int argc, wchar_t *argv[]

Обратите внимание на то, что строки в Python являются многобайтовыми, поэтому многие функции API подразумевают работу с типом данных wchar_t. В случае использования однобайтных цепочек символов используется системная локаль, как правило это UTF-8.

Программой может быть не только текст программы, хранящийся в файле, но и произвольная строка текста:

PyRun_SimpleString("a=1\nb=2\nprint(a+b)");
// будет выведено 3

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

PyRun_SimpleString("print(__file__)");

Traceback (most recent call last):
  File "<string>", line 1, in <module>
NameError: name '__file__' is not defined

API стандартных классов Python

Язык Python содержит реализацию стандартных контейнерных классов, которые являются встроенными типами Python, но их можно использовать и без интерпретации текста программы.

Базовым классом для всех объектов является класс object, которому в Си API соответствует базовый класс PyObject. Поскольку интерптетатор реализован на языке Си, который не является объектно-ориентированным, то на пользователя API возлагается ответственноть за контролем используемых типов.

Все классы и методы в PyObject API именуются следующим образом:PyКласс_Метод, а указатель на объект класса передается в качестве первого аргумента.

Примеры стандартных классов и методов:

  • PyList_Append(PyObject *list, PyObject *item) - эквивалент list.append(item)
  • PyDict_SetItem(PyObject *p, PyObject *key, PyObject *val) - эквивалент p[key]=val

Обратите внимание, что для объектов всех классов используется тип PyObject*, поэтому необходимо предварительно проверять, какой тип имеет переменная с помощью одной из функций вида PyКласс_Check(PyObject *p), которая возвращает ненулевое значение в случае принадлежности объекта к классу, и 0 в противном случае.

Соответствие стандартных типов Python префиксам функций PyObject API: list - PyList_, tuple - PyTuple_, dict - PyDict_, str - PyUnicode_, bytes - PyBytes_, file - PyFile_, int - PyLong_, float - PyFloat_.

Обратите внимание, что тип для строк называется PyUnicode, а не PyString. Это связано с тем, что во времена Python 2 строки были двух видов: однобайтные и юникодные, а в Python 3 остались только юникод-строки.

Пример использования API без интерпретатора: разбить текст на лексемы, выделяя целые числа как числа, а остальные слова оставляя строками.

Этому коду соответствует программа на Python:

text = "сейчас 23 59 не время спать"
result = []
tokens = text.split(" ")
for entry in tokens:
	try:
        number = int(entry)
        result += [number]
    except:
        result += [ entry.upper() ]
print(result)
# ['сейчас', 23, 59, 'не', 'время', 'спать']
int main()
{
    // Если не используются wchar_t*, то по умолчанию подразумевается,
    // что все однобайтные строки - в кодировке UTF-8
    static const char TheText[] = "сейчас 23 59 не время спать";

    // Инициализация API Python
    Py_Initialize();

    // Создание Python-строк из Си-строк
    PyObject *py_text = PyUnicode_FromString(TheText);    
    PyObject *py_space_symbol = PyUnicode_FromString(" ");
    
    // Создание пустого списка
    PyObject *py_result = PyList_New(0);
    // str.split(py_text, py_space_symbol, maxsplit=-1)
    PyObject *py_tokens = PyUnicode_Split(py_text, py_space_symbol, -1);
    PyObject *py_entry = NULL;
    PyObject *py_number = NULL;
    
    // Цикл по элементам списка. PyList_Size - его размер
    for (int i=0; i<PyList_Size(py_tokens); ++i) {
        // list.__getitem__(i) - этому методу соответствует оператор []
        py_entry = PyList_GetItem(py_tokens, i);
        // Попытка создать int из строки, base=10
        // В случае не успеха устанавливается ошибка ValueError
        py_number = PyLong_FromUnicodeObject(py_entry, 10);
        // Проверяем, не возникло ли исключение
        if (! PyErr_Occurred()) {
            // OK - преобразование int(py_entry) выполнено успешно
            PyList_Append(py_result, py_number);            
        }
        else {
            // Возникло исключение, оставляем просто текст
            PyList_Append(py_result, py_entry);
            // Убираем флаг ошибки, так как мы её обработали.
            // Если этого не сделать, то это исключение попадет
            // в интерпретатор, как только он будет использован
            PyErr_Clear();
        }
    }
    // Вывод print(repr(py_result))
    // Если последний параметр Py_PRINT_RAW вместо 0,
    // то вместо repr() будет использована функция str() для
    // преобразования произвольного объекта к строковому виду
    PyObject_Print(py_result, stdout, 0);

    Py_Finalize();
}

Расширениие функциональности Си-модулями

Скрипты на Python, выполняемые интерпретатором, могут использовать любые модули через import, в этом случае выполняется их поиск в одном из каталогов, перечисленных в списке sys.path. Некоторые из модулей являются встроенными (built-in), и не загружаются из внешних файлов, а создаются самим интерпретатором.

Для доступа к функциональности приложения, в который встраивается интерпретатор, можно использовать встроенные модули, которые взаимодействуют с самим приложением.

# создадим модуль с названием 'app', который реализует функциональность
import app
# модуль может содержать функции
app.do_something()
# и какие-нибудь глобальные переменные
print(app.some_value)

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

Иниализация выполняется ровно один раз, независимо от того, сколько раз импортируется модуль. Для обычных Python-модулей, его инициализация заключается в выполнении текста программы, а для встроенных модулей - в вызове функции, которая возвращает новый модуль.

static PyObject *
create_module() {
    // NULL в качестве возвразаемого значения любой функции,
    // которая должна возвращать PyObject*, означает 
    // исключительную ситуацию
    PyErr_SetString(PyExc_RuntimeError, "Not implemented yet");
    return NULL;
}    

int main(int argc, char *argv[]) {
    
    // Добавляем в таблицу имя встроенного модуля
    PyImport_AppendInittab("app", create_module);
    
    // Регистрация встроенных модулей должна быть сделана 
    // раньше, чем PyInitialize
    PyInitialize();
    ...
}

Сам модуль - это объект Python, который инициализируется из структуры-описания PyModuleDef:

static PyModuleDef moduleDef = {
	// ссылка на RTTI, поскольку Си не является ООП-языком
    .m_base = PyModuleDef_HEAD_INIT,
    // имя модуля
    .m_name = "app",
    // размер дополнительной памяти для хранения состояния модуля в
    // случае использования нескольких интерпретаторов, либо -1,
    // если не планируется использование PyModule_GetState
    .m_size = -1,
    // указатель на список методов (функций) модуля, может быть NULL
    .m_methods = methods,
};
PyObject *module = PyModule_Create(&moduleDef);

Список методов модуля - это массив объектов PyMethodDef, признаком конца которого является "нулевой элемент", - структура заполненная нулями, по аналогии с признаком конца строк в языке Си.

static PyObject *
do_something(PyObject *self, PyObject *args) {
    PyErr_SetString(PyExc_RuntimeError, "Not implemented yet");
    return NULL;
}

static PyMethodDef methods[] = {
    {
        // имя Python-функции 
        .ml_name = "do_something",
        // указатель на Си-функцию
        .ml_meth = do_something,
        // флаги использования Си-функции
        .ml_flags = METH_VARARGS,
        // строка описания, выдается функцией help()
        .ml_doc = "Do something very useful"
    },
    // признак конца массива описаний методов
    {NULL, NULL, 0, NULL}
};

Каждая Си-функция, которая реализует Python-функцию, должна возвращать объект PyObject*, и принимает минимум один аргумент - указатель на сам объект модуля.

Си-функции могут иметь разные аргументы (включая из количество), в зависимости о того, как допускается вызывать метод. Это поведение определяется флагами в поле ml_flags.

  • С одним аргументом: (PyObject *self) - в случае, если функция не принимает никаких аргументов и значение в .ml_flags = METH_NOARGS
  • С двумя аргументами: (PyObject *self, PyObject *argsTuple), причем второй аргумент является кортежем (возможно пустым), - в случае если функция принимает переменное количество позиционных аргументов и значение в .ml_flags = METH_VARARGS
  • С двумя аргументами: (PyObject *self, PyObject *argsDict), причем второй аргумент является словарем (возможно пустым), - в случае если функция принимает переменное количество именованных аргументов и значение в .ml_flags = METH_KEYWORDS
  • С тремя аргументами: (PyObject *self, PyObject *argsTuple, PyObject *argsDict), где второй аргумент - это кортеж из позиционных аргументов, а третий - это словарь именованных аргументов, - при значении .ml_flags = METH_KEYWORDS|METH_VARARGS

Возвращаемое значение NULL вместо объекта PyObject* означает исключительную ситуацию. В языке Python любая функция должна возвращать хоть какой-нибудь объект. С точки зрения синтаксиса языка, отсутствие возвращаемого значения означает, что будет возвращен объект типа NoneType, который называется None.

Объект типа None существует в единственном экзепляре на весь интерпретатор, но при этом может много где ипользоваться, и к нему, как и к любому объекту, применяются обычные правила подсчета ссылок.

def a(): pass  # функция, которая "ничего не возвращает"
b = a()        # b = None, причем вызов a() увеличил счетчик ссылок на None

a()            # возращается None, и результат отбрасывается,
               # поскольку он ничему не присвоен. В момент вызова a()
               # увеличивается счетчик ссылок, при отсутствии левой части
               # присваивания счетчик сслок уменьшается

При создании новых объектов из Си-кода, счетчик ссылок устанавливается равным 1, и обычно никаких действий по его увеличению не требуется, если объекты возвращаются интерпретатору:

static PyObject *
func_returning_string(PyObject *self)
{
    PyObject *ret = PyUnicode_FromString("Hello"); // ret->ob_refcnt=1
    return ret; // OK
}

# из Python:
a = func_returning_string()  # ret -> a, refcnt=1
func_returning_string()      # del ret, refcnt=1 --> refcnt=0    

В случае использования None, поскольку он существует в единственном экземпляре, нужно увеличить количество ссылок:

static PyObject *
func_returning_none(PyObject *self)
{
    // Py_None - это указатель на статический объект _Py_NoneStruct
    Py_INCREF(Py_None);   // Py_None->ob_refcnt ++
    return Py_None;
}    

Реализация модулей для использования штатным интерпретатором

Си-модуль для языка Python - это разделяемая библиотека, которая загружается через механизм dlopen, и поэтому должна быть скомпилирована в позиционно-независимый код (опция -fPIC компилятора).

Библиотека имеет не стандартное имя файла:

  • МОДУЛЬ.so - для Mac, Linux и *BSD. Обратите внимание на отсутствие префикса lib в имени, и кроме того, в Mac используется суффикс .so вместо .dynlib
  • МОДУЛЬ.pyd или МОДУЛЬd.pyd - для Windows. Вместо суффикса .dll используется .pyd или d.pyd (для варианта сборки с отладочной информацией).

Единственная функция, которая обязана быть реализована в библиотеке - это функция:

PyObject* PyInit_МОДУЛЬ();

Функция должна создавать и возвращать объект модуля, по аналогии с расширением интерпретатора встроенным модулем.

Если код модуля реализуется на языке C++, то необходимо отключить преобразование имен с помощью extern "C", а в случае с операционной системой Windows и компилятором MSVC - ещё и объявить функцию экспортируемой: __declspec(dllexport).

Все платформо-зависимые объявления спрятаны в макрос PyMODINIT_FUNC, значение которого определяется препроцессором. Таким образом, модуль реализуется функцией:

PyMODINIT_FUNC PyInit_my_great_module() {
	static PyModuleDef modDef = {
        .m_base = PyModuleDef_HEAD_INIT,
        .m_name = "my_great_module",
        ....
    }
    return PyModule_Create(&modDef);
}

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

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

В CMake-пакете PythonLibs определяется функция для создания цели-модуля:

find_package(PythonLibs 3.6 REQUIRED)

python_add_module(my_great_module module.c)

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

Для загрузки модуля из Python необходимо разместить его в одном из каталогов поиска модулей Python, либо рядом с файлом скрипта, который его использует. Python при этом корректно работает с символическими ссылками.

Python и отладчик GDB

Отладчик GDB позволяет ставить точки останова в любой части программы, для которой существует отладочная информация. Таким образом, если собрать модуль отдельно с опцией -g, то вместе с ним можно использовать gdb даже в том случае, если для самого интерпретатора отладочная информация отсутствует. Целевой программой для gdb указывается исполняемый файл интерпретатора python3 , а сам тестовый скрипт - в качестве аргумента запуска.

> gdb python3
(gdb) b module.c:112
(gdb) r script.py

Современные версии gdb (начиная с 7.x) включают поддержку расширений для типов данных Python API, и использование команды отладчика print вызывает метод repr языка Python для выводимых объектов, а он, в свою очередь - подразумевает вызов функции PyObject_Repr(PyObject *obj) для произвольного Python-объекта.

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

PyObject* some_function(PyObject *self, PyObject *args) {
    // <-- точка останова где-то здесь
    ....
    ....
    PyObject * value = ...
    ...
}

В данном примере отладчик инициирует ошибку нарушения сегментации, поскольку локальная переменная value ещё не инициализирована. Для того, чтобы этого избежать, есть два способа:

  • Отключить использования вызова repr для Python-объектов командой отладчика disable pretty-printer (в среде QtCreator это делается автоматически при снятии чекбокса "Use Debugging Helper" в настройках отладчика), - в этом случае все Python-объекты будут отображаться как Си-структуры.

  • Реорганизовать код таким образом, чтобы на момент остановки отладчиком все локальные переменные были инициализированы.

    PyObject* some_function(PyObject *self, PyObject *args) {
        PyObject * value = NULL; 
        ....
        // <-- точка останова после инициализации всех PyObject*
        ....
        value = ...
        ...
    }