Интеграция C и Python: запуск скриптов из программы на C

Раздел: Взаимодействие с внешними сервисами -> Вызов Python из C

Иногда возникает необходимость вызвать код Python из программы на C. Это может понадобиться для использования богатых библиотек Python (например, для анализа данных, машинного обучения) или для быстрого прототипирования. В этой статье рассмотрим несколько подходов к встраиванию Python в приложения на C, от простых системных вызовов до использования Python/C API.

Python/C API: встраивание интерпретатора

Самый надёжный и производительный способ - использовать официальный Python/C API. Для этого ваша программа должна быть скомпилирована с опцией линковки -lpython3.X и включать заголовочный файл Python.h.

Пример полного цикла:

#include <Python.h>

int main() {
    Py_Initialize();
    if (!Py_IsInitialized()) return 1;
    
    PyRun_SimpleString("import sys\nprint('Python version:', sys.version)");
    
    // Импорт модуля и вызов функции
    PyObject *pModule = PyImport_ImportModule("mymod");
    if (pModule) {
        PyObject *pFunc = PyObject_GetAttrString(pModule, "add");
        if (pFunc && PyCallable_Check(pFunc)) {
            PyObject *pArgs = Py_BuildValue("(ii)", 3, 4);
            PyObject *pResult = PyObject_CallObject(pFunc, pArgs);
            if (pResult) {
                long res = PyLong_AsLong(pResult);
                printf("3+4 = %ld\n", res);
                Py_DECREF(pResult);
            } else { PyErr_Print(); }
            Py_DECREF(pArgs); Py_DECREF(pFunc);
        } else { PyErr_Print(); }
        Py_DECREF(pModule);
    } else { PyErr_Print(); }
    
    Py_Finalize();
    return 0;
}

C call python (вызов python из c)

Пояснения: Py_Initialize запускает интерпретатор; Py_BuildValue формирует кортеж аргументов; PyObject_CallObject вызывает функцию; после использования все объекты должны быть освобождены через Py_DECREF.

Типичные проблемы и решения

  • Утечки памяти: каждый полученный объект (кроме временных) нужно освободить. Решение: использовать Py_DECREF или Py_XDECREF для каждого объекта.
  • Исключения Python: после вызова, вернувшего NULL, необходимо проверить и очистить ошибку. Решение: PyErr_Print() или PyErr_Fetch().
  • GIL в многопоточных программах: перед вызовом Python из другого потока нужно захватить GIL. Решение: PyGILState_Ensure()/PyGILState_Release().

Вариант 1: Вызов через system или popen

Как быстро выполнить Python скрипт, не углубляясь в интеграцию?

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

#include <stdio.h>

int main() {
    FILE *fp = popen("python3 -c \"print('Hello from Python')\"", "r");
    if (fp) {
        char buf[256];
        while (fgets(buf, sizeof(buf), fp)) printf("%s", buf);
        pclose(fp);
    }
    return 0;
}

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

Проблемы и решения

  • Безопасность: избегать передачи пользовательского ввода в команду. Решение: использовать аргументы через sys.argv или передавать данные через временные файлы.
  • Производительность: запуск интерпретатора занимает десятки миллисекунд. Решение: для частых вызовов лучше использовать встраивание.

Вариант 2: Динамическая загрузка Python (dlopen)

Как загрузить интерпретатор без статической линковки?

Используя dlopen и dlsym, можно загрузить библиотеку libpython во время выполнения и получить адреса её функций. Это полезно, если версия Python неизвестна на этапе компиляции.

#include <dlfcn.h>
#include <stdio.h>

int main() {
    void *handle = dlopen("libpython3.12.so.1.0", RTLD_LAZY | RTLD_GLOBAL);
    if (!handle) { fprintf(stderr, "Error: %s\n", dlerror()); return 1; }
    
    void (*Py_Initialize)(void) = dlsym(handle, "Py_Initialize");
    void (*Py_Finalize)(void) = dlsym(handle, "Py_Finalize");
    // ... другие функции
    
    Py_Initialize();
    printf("Python interpreter loaded dynamically\n");
    Py_Finalize();
    
    dlclose(handle);
    return 0;
}

Проблемы: необходимость точного имени библиотеки и экспортируемых символов; разные платформы (Windows - LoadLibrary). Решение: использовать условную компиляцию или таблицы поиска.

Вариант 3: Преобразование Python в C с помощью Cython

Как избежать встраивания интерпретатора и получить статическую библиотеку?

Cython компилирует код Python (.pyx) в C-расширение, которое можно слинковать с программой на C. При этом интерпретатор не требуется.

Пример: создадим файл mymod.pyx:

def add(int a, int b):
    return a + b

Скомпилируем: cython -3 --embed mymod.pyx (получим mymod.c). Затем скомпилируем C программу, включив mymod.o, и линкуем без libpython. Вызов функции происходит как обычная C-функция.

Проблемы: не весь Python код можно скомпилировать; требуется статическая типизация для производительности. Решение: использовать Cython только для критичных участков.

Выбор подхода зависит от конкретной задачи. Python/C API обеспечивает полную интеграцию с контролем памяти и исключений. system/popen - для быстрых прототипов. Динамическая загрузка - для гибкости версий. Cython - для максимальной производительности без интерпретатора.

Расширенные примеры вызова Python из C

Ниже приведены дополнительные примеры, демонстрирующие различные аспекты интеграции.

Пример 1: Вызов функции len() с передачей строки

Пример
#include <Python.h>

int main() {
    Py_Initialize();
    PyObject *pModule = PyImport_ImportModule("builtins");
    PyObject *pFunc = PyObject_GetAttrString(pModule, "len");
    if (pFunc && PyCallable_Check(pFunc)) {
        PyObject *pStr = PyUnicode_FromString("Hello World");
        PyObject *pArgs = PyTuple_Pack(1, pStr);
        PyObject *pResult = PyObject_CallObject(pFunc, pArgs);
        if (pResult) {
            printf("Length: %ld\n", PyLong_AsLong(pResult));
            Py_DECREF(pResult);
        }
        Py_DECREF(pArgs); Py_DECREF(pStr); Py_DECREF(pFunc);
    }
    Py_DECREF(pModule);
    Py_Finalize();
    return 0;
}

Результат выполнения:

Length: 11

Пример 2: Вызов пользовательской функции с передачей списка

Пример
// Пусть Python функция sum_list(lst) возвращает сумму элементов.
PyObject *pList = PyList_New(3);
for (int i=1; i<=3; i++)
    PyList_SetItem(pList, i-1, PyLong_FromLong(i*10));
PyObject *pArgs = PyTuple_Pack(1, pList);
PyObject *pResult = PyObject_CallObject(pFunc, pArgs);
if (pResult) {
    printf("Sum: %ld\n", PyLong_AsLong(pResult));
    Py_DECREF(pResult);
}
Py_DECREF(pArgs); Py_DECREF(pList);

Результат:

Sum: 60

Пример 3: Обработка исключения при вызове несуществующей функции

Пример
PyObject *pFunc = PyObject_GetAttrString(pModule, "nonexistent");
if (!pFunc) {
    PyObject *pType, *pValue, *pTraceback;
    PyErr_Fetch(&pType, &pValue, &pTraceback);
    const char *msg = PyUnicode_AsUTF8(PyObject_Str(pValue));
    printf("Error: %s\n", msg);
    Py_XDECREF(pType); Py_XDECREF(pValue); Py_XDECREF(pTraceback);
}

Результат:

Error: module 'mymod' has no attribute 'nonexistent'

Пример 4: Многопоточный вызов Python с использованием GIL

Пример
#include <pthread.h>
#include <Python.h>

void *thread_func(void *arg) {
    PyGILState_STATE gstate = PyGILState_Ensure();
    PyRun_SimpleString("print('Hello from thread')");
    PyGILState_Release(gstate);
    return NULL;
}

int main() {
    Py_Initialize();
    PyEval_InitThreads(); // инициализация GIL
    pthread_t th;
    pthread_create(&th, NULL, thread_func, NULL);
    pthread_join(th, NULL);
    Py_Finalize();
    return 0;
}

Результат:

Hello from thread

Пример 5: Работа с numpy массивом (требуется инициализация numpy)

Пример
#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION
#include <numpy/arrayobject.h>

int main() {
    Py_Initialize();
    import_array(); // макрос для инициализации numpy API
    npy_intp dims[] = {2,3};
    PyObject *pArray = PyArray_SimpleNew(2, dims, NPY_DOUBLE);
    double *data = (double*)PyArray_DATA(pArray);
    for (int i=0; i<6; i++) data[i] = i;
    // передаём массив в Python функцию (предполагается, что pFunc уже получена)
    PyObject *pArgs = PyTuple_Pack(1, pArray);
    PyObject *pResult = PyObject_CallObject(pFunc, pArgs);
    // обработка результата
    Py_DECREF(pArray); Py_DECREF(pArgs); Py_XDECREF(pResult);
    Py_Finalize();
    return 0;
}

Пример 6: Использование popen для выполнения скрипта и чтения вывода

Пример
FILE *fp = popen("python3 -c \"import math; print(math.sqrt(16))\"", "r");
char buf[64];
if (fp) {
    fgets(buf, sizeof(buf), fp);
    printf("sqrt(16) = %s", buf);
    pclose(fp);
}

Результат:

sqrt(16) = 4.0

Пример 7: Динамическая загрузка полного набора функций через dlopen

Пример
// Определяем указатели на функции
void (*Py_Initialize)(void);
void (*Py_Finalize)(void);
PyObject* (*Py_BuildValue)(const char*, ...);
// ... инициализация через dlsym
Py_Initialize = (void(*)(void)) dlsym(handle, "Py_Initialize");
Py_Finalize = (void(*)(void)) dlsym(handle, "Py_Finalize");
Py_BuildValue = (PyObject*(*)(const char*, ...)) dlsym(handle, "Py_BuildValue");
// Проверка на NULL после каждого dlsym
if (!Py_Initialize || !Py_Finalize || !Py_BuildValue) {
    fprintf(stderr, "Failed to load symbols: %s\n", dlerror());
    dlclose(handle);
    return 1;
}
Py_Initialize();
PyObject *pArgs = Py_BuildValue("(ii)", 2, 3);
// ... вызов функции
Py_Finalize();
dlclose(handle);

Эти примеры охватывают основные сценарии использования. Рекомендуется тщательно тестировать каждый подход на предмет утечек памяти и корректной обработки ошибок.

Вызов Python из C - comments

En
C call python (python)