Встраивание Python в C/C++: первая часть

ОГЛАВЛЕНИЕ

Статья познакомит программистов C/C++ с Python/C API, библиотекой C, помогающей встраивать модули Python в приложения C/C++. Библиотека API предоставляет множество подпрограмм C для инициализации интерпретатора Python, вызова модулей Python и завершения встраивания. Библиотека скомпилирована с Python и распространяется со всеми последними выпусками Python.

• Скачать исходные файлы - 5.23Кб
• Скачать демонстрационный проект - 80.6 Кб

Введение

Статья "Встраивание Python в многопоточные приложения C/C++" (Linux Journal) вдохновила на более глубокое освещение темы встраивания Python. Эта статья написана с двумя целями:
1. Она написана для программистов, более опытных в C/C++, чем в Python. Руководство применяет практический подход и пропускает все теоретические рассуждения.
2. Постараться сохранить межплатформенную совместимость Python при написании кода встраивания.
Имеются модули, написанные другими на Python, которые надо использовать. Вы разбираетесь в C/C++, но мало знакомы с Python. Нет инструмента для преобразования их в код C, как преобразование из FORTRAN. Однако некоторые инструменты генерируют исполнимый модуль из модуля Python. Но они не решают проблему. Преобразование кода в исполнимые модули обычно все усложняет, так как приходится выяснять, как приложение C/C++ взаимодействует с исполнимым "черным ящиком".

Первая часть серии статей рассматривает основы встраивания Python. Вторая часть переходит к более продвинутым темам. Это руководство не обучает языку Python систематически, но кратко описывается, как работает код Python, когда он появляется. Акцент будет на то, как внедрить модули Python в приложения C/C++. Смотрите статью: "Встраивание Python в C/C++: вторая часть".

Чтобы использовать исходный код, надо установить последний выпуск Python, Visual C++ (или компилятор GCC на Linux). Тесты проводились в следующей среде: Python 2.4 (Windows и Linux), Visual C++ 6.0 (Windows) или GCC 3.2 (RedHat 8.0 Linux). В случае Visual C++ выберите конфигурацию выпуска для компиляции. Конфигурация отладки требует библиотеку отладки Python "python24_d.lib", не поставляемую с нормальными сборками.

Справка

Python – мощный интерпретируемый язык, как Java, Perl и PHP. Он обладает множеством прекрасных свойств, ожидаемых программистами, в том числе "простотой" и "переносимостью". Помимо доступных инструментов и библиотек, язык Python хорошо подходит для моделирования. Он бесплатный, и инструменты и библиотеки, написанные для программистов Python, тоже бесплатные. Более подробно о языке читайте на официальном сайте.

Азы встраивания: функции, классы и методы

Сначала рассматривается пример программы на C, вызывающей функцию в модуле Python. Ниже приведен исходный файл "call_function.c":

// call_function.c – пример вызова
// функций python из кода C
//
#include <Python.h>

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

if (argc < 3)
{
printf("Usage: exe_name python_source function_name\n");
return 1;
}

// Инициализировать интерпретатор Python
Py_Initialize();

// Построить объект имени
pName = PyString_FromString(argv[1]);

// Загрузить объект модуля
pModule = PyImport_Import(pName);

// pDict – заимствованная ссылка
pDict = PyModule_GetDict(pModule);

// pFunc – тоже заимствованная ссылка
pFunc = PyDict_GetItemString(pDict, argv[2]);

if (PyCallable_Check(pFunc))
{
PyObject_CallObject(pFunc, NULL);
} else
{
PyErr_Print();
}

// Вернуть ресурсы системе
Py_DECREF(pModule);
Py_DECREF(pName);

// Завершить интерпретатор Python
Py_Finalize();

return 0;
}

Исходный файл Python "py_function.py" выглядит так:

'''py_function.py – исходник Python, служащий для'''
'''демонстрации применения встраивания python'''

def multiply():
c = 12345*6789
print 'The result of 12345 x 6789 :', c
return c

Проверки на действительность объектов пропускаются для краткости. В Windows компилируется исходник C и получается исполнимый модуль по имени "call_function.exe". Для его запуска введите в командную строку "call_function py_function multiply". Второй аргумент является именем файла Python (без расширения), который при загрузке становится именем модуля. Третий аргумент является именем функции Python, вызываемой в модуле. Исходник Python не участвует в компиляции или компоновке; он только загружается и интерпретируется во время выполнения. Вывод из выполнения следующий:

The result of 12345 x 6789 : 83810205

Сам код C не требует разъяснений, не считая того, что:

• Все в Python является объектом. pDict и pFunc являются заимствованными ссылками, поэтому не нужно Py_DECREF() их.
• Все вызовы Py_XXX и PyXXX_XXX являются вызовами Python/C API.
• Код компилируется и выполняется на всех платформах, поддерживаемых Python.

Теперь надо передать аргументы функции Python. Добавляется блок для обработки аргументов для вызова:

if (PyCallable_Check(pFunc)) 
{
// Подготовить список аргументов для вызова
if( argc > 3 )
{
pArgs = PyTuple_New(argc - 3);
for (i = 0; i < argc - 3; i++)
{
pValue = PyInt_FromLong(atoi(argv[i + 3]));
if (!pValue)
{
PyErr_Print();
return 1;
}
PyTuple_SetItem(pArgs, i, pValue);
}

pValue = PyObject_CallObject(pFunc, pArgs);

if (pArgs != NULL)
{
Py_DECREF(pArgs);
}
} else
{
pValue = PyObject_CallObject(pFunc, NULL);
}

if (pValue != NULL)
{
printf("Return of call : %d\n", PyInt_AsLong(pValue));
Py_DECREF(pValue);
}
else
{
PyErr_Print();
}

// некоторый код пропущен...
}

Новый исходник C добавляет блок "Подготовить список аргументов для вызова" и проверку возвращаемого значения. Он создает кортеж (спископодобный тип) для хранения всех параметров для вызова. Выполнение команды "call_function py_source multiply1 6 7" дает вывод:

The result of 6 x 7 : 42
Return of call : 42

Легко писать классы на Python. Также легко использовать класс Python в коде C. Надо лишь создать экземпляр объекта и вызвать его методы, так же как вызываются нормальные функции. Пример ниже:

// call_class.c – пример встраивания python
// (вызов классов python из кода C)
//
#include <Python.h>

int main(int argc, char *argv[])
{
PyObject *pName, *pModule, *pDict,
*pClass, *pInstance, *pValue;
int i, arg[2];

if (argc < 4)
{
printf(
"Usage: exe_name python_fileclass_name function_name\n");
return 1;
}

// некоторый код пропущен...

// построить имя вызываемого класса
pClass = PyDict_GetItemString(pDict, argv[2]);

// создать экземпляр класса
if (PyCallable_Check(pClass))
{
pInstance = PyObject_CallObject(pClass, NULL);
}

// построить список параметров
if( argc > 4 )
{
for (i = 0; i < argc - 4; i++)
{
arg[i] = atoi(argv[i + 4]);
}
// вызвать метод класса с двумя параметрами
pValue = PyObject_CallMethod(pInstance,
argv[3], "(ii)", arg[0], arg[1]);
} else
{
// вызвать метод класса без параметров
pValue = PyObject_CallMethod(pInstance, argv[3], NULL);
}
if (pValue != NULL)
{
printf("Return of call : %d\n", PyInt_AsLong(pValue));
Py_DECREF(pValue);
}
else
{
PyErr_Print();
}

// некоторый код пропущен...
}

Третий параметр для PyObject_CallMethod(), "(ii)" является форматирующей строкой, указывающей, что следующие аргументы – два целых числа. PyObject_CallMethod() принимает типы переменных C как свои аргументы, а не как объекты Python. Этим он отличается от других вызовов, встречавшихся до сих пор. Исходник Python "py_class.py" выглядит так:

'''py_class.py - исходник Python, служащий для'''
'''демонстрации применения встраивания python'''

class Multiply:
def __init__(self):
self.a = 6
self.b = 5

def multiply(self):
c = self.a*self.b
print 'The result of', self.a, 'x', self.b, ':', c
return c

def multiply2(self, a, b):
c = a*b
print 'The result of', a, 'x', b, ':', c
return c

Для запуска приложения добавляется имя класса между именами модуля и функции, являющейся "Multiply" в данном случае. Командная строка становится "call_class py_class Multiply multiply" или "call_class py_class Multiply multiply2 9 9".


Многопоточное встраивание Python

Пора перейти к серьезному делу. Модулю Python и приложению C/C++ порой приходится работать одновременно, например, при моделировании. Допустим, встраиваемый модуль Python является частью моделирования в реальном времени и работает параллельно с остальной частью моделирования. Между тем он взаимодействует с остальной частью во время выполнения. Общепринятый метод - многопоточность. Есть несколько вариантов многопоточного встраивания. Здесь будут рассмотрены два из них.

При одном подходе создается отдельный поток в C, и вызывается модуль Python из функции потока. Это естественно и правильно, не считая того, что приходится защищать состояние интерпретатора Python. По сути, интерпретатор Python блокируется перед его использованием и освобождается после использования, так что Python может отслеживать его состояния для разных вызывающих потоков. Python предоставляет глобальную блокировку для этой цели. Ниже приведен полный исходный код "call_thread.c":

// call_thread.c – пример встраивания python
// (поток C, вызывающий функции python)
//
#include <Python.h>
#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <errno.h>

#ifdef WIN32 // включения Windows
#include <Windows.h>
#include <process.h>
#define sleep(x) Sleep(1000*x)
HANDLE handle;
#else // включения POSIX
#include <pthread.h>
pthread_t mythread;
#endif

void ThreadProc(void*);

#define NUM_ARGUMENTS 5
typedef struct
{
int argc;
char *argv[NUM_ARGUMENTS];
} CMD_LINE_STRUCT;

int main(int argc, char *argv[])
{
int i;
CMD_LINE_STRUCT cmd;
pthread_t mythread;

cmd.argc = argc;
for( i = 0; i < NUM_ARGUMENTS; i++ )
{
cmd.argv[i] = argv[i];
}

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

// создать поток
#ifdef WIN32
// код Windows
handle = (HANDLE) _beginthread( ThreadProc,0,&cmd);
#else
// код POSIX
pthread_create( &mythread, NULL,
ThreadProc, (void*)&cmd );
#endif

// произвольный тестовый код
for(i = 0; i < 10; i++)
{
printf("Printed from the main thread.\n");
sleep(1);
}

printf("Main Thread waiting for My Thread to complete...\n");

// присоединить и ждать завершения созданного потока...
#ifdef WIN32
// код Windows
WaitForSingleObject(handle,INFINITE);
#else
// код POSIX
pthread_join(mythread, NULL);
#endif

printf("Main thread finished gracefully.\n");

return 0;
}

void ThreadProc( void *data )
{
int i;
PyObject *pName, *pModule, *pDict,
*pFunc, *pInstance, *pArgs, *pValue;
PyThreadState *mainThreadState, *myThreadState, *tempState;
PyInterpreterState *mainInterpreterState;

CMD_LINE_STRUCT* arg = (CMD_LINE_STRUCT*)data;

// произвольный тестовый код
for(i = 0; i < 15; i++)
{
printf("...Printed from my thread.\n");
sleep(1);
}

// инициализировать интерпретатор python
Py_Initialize();

// инициализировать поддержку потоков
PyEval_InitThreads();

// сохранить указатель на главный объект PyThreadState
mainThreadState = PyThreadState_Get();

// получить ссылку на PyInterpreterState
mainInterpreterState = mainThreadState->interp;

// создать объект состояния потока для этого потока
myThreadState = PyThreadState_New(mainInterpreterState);

// снять глобальную блокировку
PyEval_ReleaseLock();

// получить глобальную блокировку
PyEval_AcquireLock();

// загрузить мое состояние потока
tempState = PyThreadState_Swap(myThreadState);

// выполнить некоторый код python (вызвать функции python)
pName = PyString_FromString(arg->argv[1]);
pModule = PyImport_Import(pName);

// pDict и pFunc – заимствованные ссылки
pDict = PyModule_GetDict(pModule);
pFunc = PyDict_GetItemString(pDict, arg->argv[2]);

if (PyCallable_Check(pFunc))
{
pValue = PyObject_CallObject(pFunc, NULL);
}
else {
PyErr_Print();
}

// возврат ресурсов системе
Py_DECREF(pModule);
Py_DECREF(pName);

// выгрузить текущий поток
PyThreadState_Swap(tempState);

// снять глобальную блокировку
PyEval_ReleaseLock();

// очистить состояние потока
PyThreadState_Clear(myThreadState);
PyThreadState_Delete(myThreadState);

Py_Finalize();
printf("My thread is finishing...\n");

// выход из потока
#ifdef WIN32
// код Windows
_endthread();
#else
// код POSIX
pthread_exit(NULL);
#endif
}

Функция потока требует немного пояснений. PyEval_InitThreads() инициализирует поддержку потоков Python. PyThreadState_Swap(myThreadState) загружает состояние для текущего потока, а PyThreadState_Swap(tempState) выгружает его. Интерпретатор Python сохраняет то, что происходит между двумя вызовами, как данные о состоянии, связанном с этим потоком. Вообще, Python сохраняет данные для каждого потока, использующего интерпретатор, так что состояния потоков взаимоисключающие. Но необходимо создать и сохранять состояние для каждого потока C. Сначала не был вызван PyEvel_AcquireLock(), потому что PyEval_InitThreads() делает это по умолчанию. В других случаях надо использовать PyEvel_AcquireLock() и PyEvel_ReleaseLock() попарно.

Выполните "call_thread py_thread pythonFunc" и получите показанный ниже вывод. Файл "py_thread.py" определяет функцию по имени pythonFunc(), в которой похожий произвольный тестовый блок печатает "распечатано из pythonFunc..." на экране 15 раз.

Printed from the main thread.
...Printed from my thread.
Printed from the main thread.
...Printed from my thread.
Printed from the main thread.
...Printed from my thread.
Printed from the main thread.
...Printed from my thread.
Printed from the main thread.
...Printed from my thread.
...Printed from my thread.
Printed from the main thread.
Printed from the main thread.
...Printed from my thread.
Printed from the main thread.
...Printed from my thread.
Printed from the main thread.
...Printed from my thread.
Printed from the main thread.
...Printed from my thread.
Main Thread waiting for My Thread to complete...
...Printed from my thread.
...Printed from my thread.
...Printed from my thread.
...Printed from my thread.
...Printed from my thread.
My thread is finishing...
Main thread finished gracefully.

Разумеется, реализация усложняется, потому что писать многопоточный код на C/C++ непросто. Несмотря на то, что код переносимый, он содержит многочисленные «заплаты», требующие досконального знания интерфейсов вызова системы для конкретных платформ. К счастью, Python сделал большую часть этого, что приводит к рассмотрению второго решения, а именно, позволить Python управлять многопоточностью. На этот раз код Python усовершенствован путем добавления модели поточной обработки:

'''Показать использование поточной обработки python'''
import time
import threading

class MyThread(threading.Thread):
def __init__(self):
threading.Thread.__init__(self)
def run(self):
for i in range(15):
print 'printed from MyThread...'
time.sleep(1)

def createThread():
print 'Create and run MyThread'
background = MyThread()
background.start()
print 'Main thread continues to run in foreground.'
for i in range(10):
print 'printed from Main thread.'
time.sleep(1)
print 'Main thread joins MyThread and waits until it is done...'
background.join() # ждать завершения фоновой задачи
print 'The program completed gracefully.'

Код C больше не управляет поточной обработкой. Он только вызывает createThread(). Попробуйте использовать предыдущий "call_function.c". Запустите "call_function py_thread createThread", чтобы увидеть, как выглядит вывод. Второе решение опрятней и проще. Более того, модель поточной обработки Python переносима. В то время как код поточной обработки C для Unix и Windows различается, в Python он остается неизменным.

Функция Python createThread() не нужна, если код C вызывает методы start() и joint() класса потока. Соответствующие изменения перечислены ниже (из исходного файла C "call_thread_2.c"):

// создать экземпляр
pInstance = PyObject_CallObject(pClass, NULL);

PyObject_CallMethod(pInstance, "start", NULL);

i = 0;
while(i<10)
{
printf("Printed from C thread...\n");

// !!!важно!!! поток C не освободит процессор для
// потока Python без следующего вызова.
PyObject_CallMethod(pInstance, "join", "(f)", 0.001);
Sleep(1000);
i++;
}

printf(
"C thread join and wait for Python thread to complete...\n");
PyObject_CallMethod(pInstance, "join", NULL);

printf("Program completed gracefully.\n");

После создания экземпляра класса вызывается его метод start() для создания нового потока, и выполняется его метод run(). Без частых коротких присоединений к созданному потоку созданный поток может выполниться только в начале, и главный поток не освободит для него процессор, пока не завершится. Можно проверить это, закомментировав вызов присоединения в цикле while. Поведение отличается от предыдущего случая, где вызывался start() из модуля Python. Видимо, это свойство многопоточности, не документированное в справочнике по библиотеке Python.

Интересные особенности

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

Несмотря на то, что встраивание является хорошим вариантом использования модулей Python в приложениях C/C++, есть другие альтернативные подходы. В Windows некоторые инструменты (вроде "py2exe") преобразуют модули Python непосредственно в исполнимые модули Windows. Затем порождается процесс, чтобы запустить исполнимый модуль из приложения C/C++. Недостаток в том, что нельзя вызвать модуль напрямую. Приложению приходится взаимодействовать с модулем через определенные типы взаимодействия между процессами (IPC). При этом рассматриваемый модуль Python должен быть "готов к IPC", то есть реализовывать интерфейс IPC для обработки входящих и исходящих данных. Вторая часть этой статьи рассмотрит связанные с встраиванием приемы IPC.

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

Заключение

В этой части было рассмотрено встраивание Python от азов, таких как вызов функций, классов и методов, до более трудных тем типа многопоточного встраивания. Python/C API предоставляет логичный интерфейс вызова, облегчающий объединение C/C++ и модулей Python.

Обсуждение многопоточного встраивания поставило вопрос: как приложение C/C++ взаимодействует со встроенным модулем Python? Вторая часть данной статьи рассматривает этот вопрос с позиции IPC.