Подход к снаряду
Не так давно, в феврале, у меня случился замечательный день: ещё
один проект-долгострой "полетел". О чём речь? Речь о моей давней
задумке на тему использования интерпретатора Python в программах на
C: я реализовал добавление хуков на Python в ReOpenLDAP. Сама по себе тема,
понятное дело, большая, поэтому в таких ситуациях я пишу
минимальный код на C, который служит как раз для проверки концепта
и его обкатки - его очень удобно запускать под инструментами типа
Valgrind, которые незамедлительно укажут на явные ошибки
ляпы в работе с памятью. Однако после окончания работы я понял, что
сам по себе минимальный код может быть полезен кому-то ещё, кроме
меня. Почему? Потому перед началом работы я наивно предполагал, что
официальная документация по C
API поможет всё сделать легко и быстро, но увы! - внятного
примера с пошаговым разбором не нашёл. Что ж, это open source,
детка, не нравится - сделай сам.
Для большей точности: пример разрабатывался на 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, использованкак костыльдля обозначения конца массива.
Дальше объявляем несколько указателей на PyObject - пока что они нам не принципиальны, просто имейте в виду, что они есть.
Готовим интерпретатор к работе:
Py_Initialize();
HERE BE DRAGONS
Помните, что в программе фактически придётся управлять памятью
сразу в двух местах: на уровне кучи (malloc/free
), и
на уровне чёрного ящика интерпретатора Питона. Объекты,
возвращаемые функциями интерпретатора, будут размещёны в памяти, им
же и управляемой, поэтому придётся периодически сообщать
интерпретатору Python, что тот или иной объект мы больше не
используем, и можно его добавить в список для garbage collector'а.
Для этого нам пригодится вызов
Py_XDECREF(*Py_Object).
Он умеет сам проверять, не NULL
ли передан в параметре, и если да - функция не делает ничего, в
отличие от Py_DECREF(*Py_Object)
, которая в этом
случае вернёт ошибку.
Далее загружаем модуль sys
и добавляем в его список
path
нужный нам путь:
// credits to https://stackoverflow.com/questions/50198057/python-c-api-free-errors-after-using-py-setpath-and-py-getpath// get handle to python sys.path objectsys = PyImport_ImportModule("sys");path = PyObject_GetAttrString(sys, "path");// make a list of paths to add to sys.pathnewPaths = PyUnicode_Split(PyUnicode_FromString(hook_file_path), PyUnicode_FromWideChar(L":", 1), -1);// iterate through list and add all pathsfor(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[i]);if (pFunc && PyCallable_Check(pFunc)) { fprintf(stderr,"function %s exists and can be called\n", hook_functions[i]); fprintf(stderr, "Calling %s\n", hook_functions[i]); pValue = PyObject_CallFunction(pFunc, "s", hook_functions[i]);
Обратите внимание: после получения объекта по имени всегда полезно проверить, можем ли мы к нему обратиться. Именно это делает вторая строка. А пятая строка этого фрагмента вызывает функцию, передавая ей аргумент типа "строка" (на это указывает "s"). Для удобства каждая функция нашего кода на Python будет вызываться с единственным строковым аргументом, равным названию этой самой функции.
Вообще по
документацииPyObject_CallFunction
ровно так и
вызывается:
-
первый параметр - объект вызываемой функции в Python-коде, ранее полученный через
PyObject_GetAttrString
; -
второй, строка - сообщает интерпретатору тип и количество аргументов (более подробно об этой строке - в документации);
-
третий и далее аргументы - аргументы, то есть то, что наша Python-функция получит внутри входного кортежа (питонистам это известно как
*args
).
Итак, ссылка на объект, содержащий в себе то, что вернул наш код
на Python - в pyValue
. Можно праздновать?... Нет,
рано. Переходим к следующей части.
Разбор результата
Всё многообразие возвращаемых результатов можно свести к базовым типам - их и будем разбирать. Очередной фрагмент кода из-за длины под спойлером.
Осторожно, код
if (pValue != NULL) { if (pValue == Py_None) { fprintf(stderr,"==> Дружище, это None, тут правда ничего нет\n"); } else if ((pValue == Py_False) || (pValue == Py_True)) { fprintf(stderr,"==> Bool:\n"); if (pValue == Py_False) { fprintf(stderr, " False\n"); } else { fprintf(stderr, " True \n"); } } else if (PyUnicode_Check(pValue)) { fprintf(stderr,"==> String:\n"); const char* newstr = PyUnicode_AsUTF8(pValue); fprintf(stderr,"\"%s\"\n", newstr); } else if (PyDict_Check(pValue)) { PyObject *key, *value; Py_ssize_t pos =0; fprintf(stderr,"==> Dict:\n"); while (PyDict_Next(pValue, &pos, &key, &value)) { fprintf(stderr, "%s: %s\n", PyUnicode_AsUTF8(key), PyUnicode_AsUTF8(value)); } } else if (PyList_Check(pValue)) { fprintf(stderr,"==> List:\n"); Py_ssize_t i, seq_len; PyObject *item; seq_len = PyList_Size(pValue); for (i=0; i<seq_len; i++) { item = PyList_GetItem(pValue, i); fprintf(stderr, " %s\n", PyUnicode_AsUTF8(item)); // !!!--> NOT NEEDED <--!!! Py_DECREF(item); } } else if (PyTuple_Check(pValue)) { fprintf(stderr,"==> Tuple:\n"); Py_ssize_t i, seq_len; PyObject *item; seq_len = PyTuple_Size(pValue); for (i=0; i<seq_len; i++) { item = PyTuple_GetItem(pValue, i); fprintf(stderr, " %s\n", PyUnicode_AsUTF8(item)); // !!!--> NOT NEEDED <--!!! Py_DECREF(item); } } else if (PyFloat_Check(pValue)) { fprintf(stderr, "==> Float: %f\n", PyFloat_AsDouble(pValue)); } else if (PyLong_Check(pValue)) { fprintf(stderr, "==> Long: %ld\n", PyLong_AsLong(pValue)); } else if (PySet_Check(pValue)) { fprintf(stderr,"==> Set:\n"); PyObject *str_repr = PyObject_Repr(pValue); fprintf(stderr, " %s\n", PyUnicode_AsUTF8(str_repr)); Py_XDECREF(str_repr); } else { fprintf(stderr, "==> Какая-то дичь! Проверь-ка тип результата функции %s\n", hook_functions[i]); } Py_XDECREF(pValue);} else { fprintf(stderr, "WTF");}
Некоторые важные моменты по поводу разбора результатов, возвращаемых функциями:
-
особняком стоят значения None, True и False: для них нет каких-то отдельных проверочных функций, и мы в коде на C проверяем, не они ли это, простым сравнением со специальными константами:
Py_None, Py_True, Py_False
; -
значения-словари для иллюстрации статьи обойдём встроенным итератором, но вообще, конечно, можем получить нужный элемент по ключу;
-
для списков и кортежей функции вида PyXXXX_GetItem возвращают "чужие" ссылки - то есть вместе с ними вашему коду на C не передаётся ни владение объектом, ни обязанность этот объект уничтожить через
Py_DECREF()
-
если реализовать поддержку не конкретных типов, а протоколов - ваш C-код получит способность поддерживать питонячью утиную типизацию.
Пока писал предыдущую часть - понял, что идеальный вариант для описания функций разбора результатов - табличка-шпаргалка (под спойлером).
Функции обработки результатов
Значение в Python |
Проверка |
|
|
|
нет |
строка |
|
|
словарь |
|
|
список |
|
|
кортеж |
|
|
число с плавающей точкой |
|
|
целое число |
|
|
Заключение
В статье намеренно не освещались вопросы передачи каких-нибудь хитросложенных аргументов, объявления объектов-типов внутри интерпретатора и тому подобные вещи, включая обработку ошибок Python-кода - это всё-таки crash-course, а не олимпиада. Поэтому на этом откланиваюсь, и могу только добавить, что весь код лежит в репозитории на моём ГитХабе, а в комментариях попробую ответить на вопросы по теме статьи.
UPD. Поправил очепятку (Py_DECREF(*Py_Object) -> Py_DECREF(*Py_Object)).