Интеграция 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);Эти примеры охватывают основные сценарии использования. Рекомендуется тщательно тестировать каждый подход на предмет утечек памяти и корректной обработки ошибок.