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. В дальнейшем вы сможете вызывать интерпретатор из любой части приложения.

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

См.также

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

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

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

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

#define PY_SSIZE_T_CLEAN
#include <Python.h>

int
main(int argc, char *argv[])
{
    wchar_t *program = Py_DecodeLocale(argv[0], NULL);
    if (program == NULL) {
        fprintf(stderr, "Fatal error: cannot decode argv[0]\n");
        exit(1);
    }
    Py_SetProgramName(program);  /* optional but recommended */
    Py_Initialize();
    PyRun_SimpleString("from time import time,ctime\n"
                       "print('Today is', ctime(time()))\n");
    if (Py_FinalizeEx() < 0) {
        exit(120);
    }
    PyMem_RawFree(program);
    return 0;
}

Функция Py_SetProgramName() должна быть вызвана до Py_Initialize(), чтобы сообщить интерпретатору пути к библиотекам времени выполнения 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 (файлы .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'
Вернуться на верх