Подход к снаряду
Не так давно, в феврале, у меня случился замечательный день: ещё один проект-долгострой "полетел". О чём речь? Речь о моей давней задумке на тему использования интерпретатора Python в программах на C: я реализовал добавление хуков на Python в ReOpenLDAP. Сама по себе тема, понятное дело, большая, поэтому в таких ситуациях я пишу минимальный код на C, который служит как раз для проверки концепта и его обкатки - его очень удобно запускать под инструментами типа Valgrind, которые незамедлительно укажут на явныеДля большей точности: пример разрабатывался на CentOS 7 с установленными пакетами python3, python3-devel, то есть всё описанное было написано, отлажено и
Для удобства весь разбираемый код лежит в репозитории на моём ГитХабе.
Инициализация
Начало начал - подключение к нашей программе заголовочного файла:#include <Python.h>
Далее готовим нужные константы (понятно, что у вас их значения будут другими, просто следите за смыслом значений):
char hook_file_path[] = "./";
char hook_file[] = "ldap_hooks";
char *hook_functions[] = {
"add_hook","bind_hook","unbind_hook","compare_hook","delete_hook","modify_hook",
"modrdn_hook","search_hook","abandon_hook","extended_hook","response_hook",NULL };
PyObject *pName, *pModule, *pFunc, *pValue, *sys, *path, *newPaths;
Здесь:
- hook_file_path - каталог в файловой системе, в котором вы хотите хранить свой код на Python;
- hook_file- имя файла с кодом, расширение «.py» указывать не надо;
- hook_functions - массив с названиями функций в файле, которые мы будем искать и вызывать; последний элемент, NULL, использован
как костыльдля обозначения конца массива.
Готовим интерпретатор к работе:
Py_Initialize();
HERE BE DRAGONS
Далее загружаем модуль sys и добавляем в его список path нужный нам путь:
// credits to https://stackoverflow.com/questions...-errors-after-using-py-setpath-and-py-getpath
// get handle to python sys.path object
sys = PyImport_ImportModule("sys");
path = PyObject_GetAttrString(sys, "path");
// make a list of paths to add to sys.path
newPaths = PyUnicode_Split(PyUnicode_FromString(hook_file_path), PyUnicode_FromWideChar(L":", 1), -1);
// iterate through list and add all paths
for(i=0; i<PyList_Size(newPaths); i++) {
PyList_Append(path, PyList_GetItem(newPaths, i));
}
Py_XDECREF(newPaths);
Py_XDECREF(path);
Py_XDECREF(sys);
Я не делаю какого-то большого секрета из того факта, что часть этого кода взята со StackOverflow: что же тут поделать, ищу специфические вещи, которые редко в полном объёме покрываются документацией.
Загрузка файла с Python-кодом
Дальше будет чуть попроще - всего лишь выполним import для нашего модуля. Почему так - ровно потому, что налицо проблема курицы и яйца: некому вызвать import ldap_hooks.pName = PyUnicode_DecodeFSDefault(hook_file);
if (pName == NULL){
fprintf(stderr,"No Python hook file found\n");
return 1;
}
pModule = PyImport_Import(pName);
Py_DECREF(pName);
// fprintf(stderr,"No C errors until now\n");
Поиск функций в файле и их вызов
Итак, теперь у нас есть загруженный в память интерпретатор, готовый к работе, а его состояние соответствует тому, как если бы мы из кода на Python вызвали import для файла, чьё имя указано в строке hook_file.Далее получаем объект нужной функции и вызываем её:
pFunc = PyObject_GetAttrString(pModule,hook_functions);
if (pFunc && PyCallable_Check(pFunc)) {
fprintf(stderr,"function %s exists and can be called\n", hook_functions);
fprintf(stderr, "Calling %s\n", hook_functions);
pValue = PyObject_CallFunction(pFunc, "s", hook_functions);
Обратите внимание: после получения объекта по имени всегда полезно проверить, можем ли мы к нему обратиться. Именно это делает вторая строка. А пятая строка этого фрагмента вызывает функцию, передавая ей аргумент типа "строка" (на это указывает "s"). Для удобства каждая функция нашего кода на Python будет вызываться с единственным строковым аргументом, равным названию этой самой функции.
Вообще по документацииPyObject_CallFunction ровно так и вызывается:
- первый параметр - объект вызываемой функции в Python-коде, ранее полученный через PyObject_GetAttrString;
- второй, строка - сообщает интерпретатору тип и количество аргументов (более подробно об этой строке - в документации);
- третий и далее аргументы - аргументы, то есть то, что наша Python-функция получит внутри входного кортежа (питонистам это известно как *args).
Разбор результата
Всё многообразие возвращаемых результатов можно свести к базовым типам - их и будем разбирать. Очередной фрагмент кода из-за длины под спойлером.Осторожно, код
Некоторые важные моменты по поводу разбора результатов, возвращаемых функциями:
- особняком стоят значения None, True и False: для них нет каких-то отдельных проверочных функций, и мы в коде на C проверяем, не они ли это, простым сравнением со специальными константами: Py_None, Py_True, Py_False;
- значения-словари для иллюстрации статьи обойдём встроенным итератором, но вообще, конечно, можем получить нужный элемент по ключу;
- для списков и кортежей функции вида PyXXXX_GetItem возвращают "чужие" ссылки - то есть вместе с ними вашему коду на C не передаётся ни владение объектом, ни обязанность этот объект уничтожить через Py_DECREF()
- если реализовать поддержку не конкретных типов, а протоколов - ваш C-код получит способность поддерживать питонячью «утиную типизацию».
Функции обработки результатов
Заключение
В статье намеренно не освещались вопросы передачи каких-нибудь хитросложенных аргументов, объявления объектов-типов внутри интерпретатора и тому подобные вещи, включая обработку ошибок Python-кода - это всё-таки crash-course, а не олимпиада. Поэтому на этом откланиваюсь, и могу только добавить, что весь код лежит в репозитории на моём ГитХабе, а в комментариях попробую ответить на вопросы по теме статьи.UPD. Поправил очепятку (Py_DECREF(*Py_Object) -> Py_DECREF(*Py_Object)).
Источник статьи: https://habr.com/ru/post/543636/