1. Встраивание Python в другое приложение

В предыдущих главах обсуждалось расширение Python, то есть расширение функциональности Python за счет присоединения к нему библиотеки функций языка C. Можно сделать и наоборот: обогатить ваше приложение на C/C++, встроив в него Python. Встраивание дает возможность реализовать часть функциональности вашего приложения на Python, а не на C или C++. Это может быть использовано для многих целей; одним из примеров может быть предоставление пользователям возможности адаптировать приложение под свои нужды, написав некоторые скрипты на Python. Вы также можете использовать его самостоятельно, если некоторые функциональные возможности проще написать на Python.

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

Если вы внедряете Python, вы предоставляете свою собственную главную программу. Одна из вещей, которую должна делать эта главная программа, - инициализировать интерпретатор Python. Как минимум, вы должны вызвать функцию Py_Initialize(). Существуют необязательные вызовы для передачи аргументов командной строки в Python. В дальнейшем вы сможете вызывать интерпретатор из любой части приложения.

Существует несколько различных способов вызова интерпретатора: вы можете передать строку, содержащую операторы Python, в PyRun_SimpleString(), или передать указатель файла stdio и имя файла (только для идентификации в сообщениях об ошибках) в PyRun_SimpleFile(). Вы также можете вызывать операции нижнего уровня, описанные в предыдущих главах, для создания и использования объектов Python.

См.также

Справочное руководство по API Python/C

Подробности интерфейса Python на языке C изложены в этом руководстве. Большое количество необходимой информации можно найти здесь.

1.1. Очень высокий уровень встраивания

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

#define PY_SSIZE_T_CLEAN
#include <Python.h>

int
main(int argc, char *argv[])
{
    PyStatus status;
    PyConfig config;
    PyConfig_InitPythonConfig(&config);

    /* optional but recommended */
    status = PyConfig_SetBytesString(&config, &config.program_name, argv[0]);
    if (PyStatus_Exception(status)) {
        goto exception;
    }

    status = Py_InitializeFromConfig(&config);
    if (PyStatus_Exception(status)) {
        goto exception;
    }
    PyConfig_Clear(&config);

    PyRun_SimpleString("from time import time,ctime\n"
                       "print('Today is', ctime(time()))\n");
    if (Py_FinalizeEx() < 0) {
        exit(120);
    }
    return 0;

  exception:
     PyConfig_Clear(&config);
     Py_ExitStatusException(status);
}

Примечание

#define PY_SSIZE_T_CLEAN использовался для указания на то, что в некоторых API вместо int следует использовать Py_ssize_t. Начиная с Python 3.13 в нем нет необходимости, но мы оставляем его здесь для обратной совместимости. Описание этого макроса см. в Строки и буферы.

Установка PyConfig.program_name должна быть вызвана до Py_InitializeFromConfig(), чтобы сообщить интерпретатору пути к библиотекам времени выполнения Python. Далее интерпретатор Python инициализируется с помощью Py_Initialize(), после чего выполняется жестко закодированный Python-скрипт, печатающий дату и время. После этого вызов Py_FinalizeEx() закрывает интерпретатор, и программа завершается. В реальной программе вам может понадобиться получить сценарий Python из другого источника, возможно, из программы текстового редактора, файла или базы данных. Получить код Python из файла можно с помощью функции PyRun_SimpleFile(), которая избавит вас от необходимости выделять место в памяти и загружать содержимое файла.

1.2. За пределами встраивания очень высокого уровня: Обзор

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

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

  1. Преобразование значений данных из языка Python в язык C,

  2. Выполните вызов функции в программе на языке C, используя преобразованные значения, и

  3. Преобразуйте значения данных из вызова на языке C в Python.

При встраивании Python это делает код интерфейса:

  1. Преобразование значений данных из языка C в язык Python,

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

  3. Преобразуйте значения данных из вызова из Python в C.

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

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

1.3. Чистое встраивание

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

Код для запуска функции, определенной в сценарии Python, выглядит следующим образом:

#define PY_SSIZE_T_CLEAN
#include <Python.h>

int
main(int argc, char *argv[])
{
    PyObject *pName, *pModule, *pFunc;
    PyObject *pArgs, *pValue;
    int i;

    if (argc < 3) {
        fprintf(stderr,"Usage: call pythonfile funcname [args]\n");
        return 1;
    }

    Py_Initialize();
    pName = PyUnicode_DecodeFSDefault(argv[1]);
    /* Error checking of pName left out */

    pModule = PyImport_Import(pName);
    Py_DECREF(pName);

    if (pModule != NULL) {
        pFunc = PyObject_GetAttrString(pModule, argv[2]);
        /* pFunc is a new reference */

        if (pFunc && PyCallable_Check(pFunc)) {
            pArgs = PyTuple_New(argc - 3);
            for (i = 0; i < argc - 3; ++i) {
                pValue = PyLong_FromLong(atoi(argv[i + 3]));
                if (!pValue) {
                    Py_DECREF(pArgs);
                    Py_DECREF(pModule);
                    fprintf(stderr, "Cannot convert argument\n");
                    return 1;
                }
                /* pValue reference stolen here: */
                PyTuple_SetItem(pArgs, i, pValue);
            }
            pValue = PyObject_CallObject(pFunc, pArgs);
            Py_DECREF(pArgs);
            if (pValue != NULL) {
                printf("Result of call: %ld\n", PyLong_AsLong(pValue));
                Py_DECREF(pValue);
            }
            else {
                Py_DECREF(pFunc);
                Py_DECREF(pModule);
                PyErr_Print();
                fprintf(stderr,"Call failed\n");
                return 1;
            }
        }
        else {
            if (PyErr_Occurred())
                PyErr_Print();
            fprintf(stderr, "Cannot find function \"%s\"\n", argv[2]);
        }
        Py_XDECREF(pFunc);
        Py_DECREF(pModule);
    }
    else {
        PyErr_Print();
        fprintf(stderr, "Failed to load \"%s\"\n", argv[1]);
        return 1;
    }
    if (Py_FinalizeEx() < 0) {
        return 120;
    }
    return 0;
}

Этот код загружает скрипт Python, используя argv[1], и вызывает функцию, названную в argv[2]. Ее целочисленные аргументы - это остальные значения массива argv. Если вы compile and link эту программу (назовем готовый исполняемый файл call) используете для выполнения сценария Python, например:

def multiply(a,b):
    print("Will compute", a, "times", b)
    c = 0
    for i in range(0, a):
        c = c + b
    return c

тогда результат должен быть таким:

$ call multiply multiply 3 2
Will compute 3 times 2
Result of call: 6

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

Py_Initialize();
pName = PyUnicode_DecodeFSDefault(argv[1]);
/* Error checking of pName left out */
pModule = PyImport_Import(pName);

После инициализации интерпретатора скрипт загружается с помощью PyImport_Import(). Этой процедуре в качестве аргумента нужна строка Python, которая строится с помощью процедуры преобразования данных PyUnicode_FromString().

pFunc = PyObject_GetAttrString(pModule, argv[2]);
/* pFunc is a new reference */

if (pFunc && PyCallable_Check(pFunc)) {
    ...
}
Py_XDECREF(pFunc);

Когда скрипт загружен, искомое имя извлекается с помощью PyObject_GetAttrString(). Если имя существует, а возвращаемый объект является вызываемым, можно смело предположить, что это функция. Затем программа переходит к построению кортежа аргументов обычным образом. Вызов функции Python осуществляется с помощью команды:

pValue = PyObject_CallObject(pFunc, pArgs);

По возвращении функции pValue становится либо NULL, либо содержит ссылку на возвращаемое значение функции. Не забудьте освободить ссылку после изучения значения.

1.4. Расширение встроенного Python

До сих пор встроенный интерпретатор Python не имел доступа к функциональности самого приложения. API Python позволяет это сделать, расширяя встроенный интерпретатор. То есть встроенный интерпретатор расширяется за счет процедур, предоставляемых приложением. Хотя это звучит сложно, на самом деле все не так плохо. Просто забудьте на время, что приложение запускает интерпретатор Python. Вместо этого рассматривайте приложение как набор подпрограмм и напишите код-клея, который предоставляет Python доступ к этим подпрограммам, точно так же, как вы бы написали обычное расширение Python. Например:

static int numargs=0;

/* Return the number of arguments of the application command line */
static PyObject*
emb_numargs(PyObject *self, PyObject *args)
{
    if(!PyArg_ParseTuple(args, ":numargs"))
        return NULL;
    return PyLong_FromLong(numargs);
}

static PyMethodDef EmbMethods[] = {
    {"numargs", emb_numargs, METH_VARARGS,
     "Return the number of arguments received by the process."},
    {NULL, NULL, 0, NULL}
};

static PyModuleDef EmbModule = {
    PyModuleDef_HEAD_INIT, "emb", NULL, -1, EmbMethods,
    NULL, NULL, NULL, NULL
};

static PyObject*
PyInit_emb(void)
{
    return PyModule_Create(&EmbModule);
}

Вставьте приведенный выше код непосредственно над функцией main(). Также вставьте следующие два оператора перед вызовом функции Py_Initialize():

numargs = argc;
PyImport_AppendInittab("emb", &PyInit_emb);

Эти две строки инициализируют переменную numargs и делают функцию emb.numargs() доступной для встроенного интерпретатора Python. С помощью этих расширений сценарий Python может выполнять такие действия, как

import emb
print("Number of arguments", emb.numargs())

В реальном приложении методы будут раскрывать API приложения для Python.

1.5. Встраивание Python в C++

Можно также встроить Python в программу на C++; как именно это сделать, зависит от особенностей используемой системы C++; в общем случае вам нужно будет написать основную программу на C++ и использовать компилятор C++ для компиляции и компоновки программы. Нет необходимости перекомпилировать сам Python с помощью C++.

1.6. Компиляция и компоновка в Unix-подобных системах

Поиск правильных флагов, которые нужно передать компилятору (и компоновщику), чтобы встроить интерпретатор Python в ваше приложение, не всегда тривиален, особенно потому, что Python должен загружать библиотечные модули, реализованные в виде динамических расширений C (файлы:file:.so), скомпонованные с ним.

Чтобы узнать необходимые флаги компилятора и компоновщика, вы можете выполнить скрипт pythonX.Y-config, который генерируется в процессе установки (также может быть доступен скрипт python3-config). Этот скрипт имеет несколько опций, из которых следующие будут непосредственно полезны для вас:

  • pythonX.Y-config --cflags даст вам рекомендуемые флаги при компиляции:

    $ /opt/bin/python3.11-config --cflags
    -I/opt/include/python3.11 -I/opt/include/python3.11 -Wsign-compare  -DNDEBUG -g -fwrapv -O3 -Wall
    
  • pythonX.Y-config --ldflags --embed даст вам рекомендуемые флаги при соединении:

    $ /opt/bin/python3.11-config --ldflags --embed
    -L/opt/lib/python3.11/config-3.11-x86_64-linux-gnu -L/opt/lib -lpython3.11 -lpthread -ldl  -lutil -lm
    

Примечание

Чтобы избежать путаницы между несколькими установками Python (и особенно между системным Python и вашим собственным скомпилированным Python), рекомендуется использовать абсолютный путь к pythonX.Y-config, как в примере выше.

Если эта процедура вам не подходит (не гарантируется, что она будет работать для всех Unix-подобных платформ; однако мы приветствуем bug reports), вам придется прочитать документацию вашей системы о динамическом связывании и/или изучить Makefile (используйте sysconfig.get_makefile_filename(), чтобы найти его расположение) и параметры компиляции Python. В этом случае модуль sysconfig является полезным инструментом для программного извлечения значений конфигурации, которые вы хотите объединить вместе. Например:

>>> import sysconfig
>>> sysconfig.get_config_var('LIBS')
'-lpthread -ldl  -lutil'
>>> sysconfig.get_config_var('LINKFORSHARED')
'-Xlinker -export-dynamic'