Как стать автором
Обновить

C и Python: мост между мирами

Время на прочтение6 мин
Количество просмотров9.2K

Подход к снаряду

Не так давно, в феврале, у меня случился замечательный день: ещё один проект-долгострой "полетел". О чём речь? Речь о моей давней задумке на тему использования интерпретатора 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 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[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

Проверка

Использование в C

None

True

False

pValue == Py_None

pValue == PyTrue

pValue == Py_False

нет

строка

PyUnicode_Check(pValue)

PyUnicode_AsUTF8(pValue)

словарь

PyDict_Check(pValue)

PyObject *key, *value;
Py_ssize_t pos =0;
while ( PyDict_Next(
pValue, &pos, &key, &value)) {
.....
}

список

PyList_Check(pValue)

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);
.....}

кортеж

PyTuple_Check(pValue)

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);
.....}

число с плавающей точкой

PyFloat_Check(pValue)

PyFloat_AsDouble(pValue)

целое число

PyLong_Check(pValue)

PyLong_AsLong(pValue)

Заключение

В статье намеренно не освещались вопросы передачи каких-нибудь хитросложенных аргументов, объявления объектов-типов внутри интерпретатора и тому подобные вещи, включая обработку ошибок Python-кода - это всё-таки crash-course, а не олимпиада. Поэтому на этом откланиваюсь, и могу только добавить, что весь код лежит в репозитории на моём ГитХабе, а в комментариях попробую ответить на вопросы по теме статьи.

UPD. Поправил очепятку (Py_DECREF(*Py_Object) -> Py_DECREF(*Py_Object)).

Теги:
Хабы:
Если эта публикация вас вдохновила и вы хотите поддержать автора — не стесняйтесь нажать на кнопку
Всего голосов 6: ↑4 и ↓2+3
Комментарии5

Публикации

Истории

Работа

Python разработчик
117 вакансий
Программист С
37 вакансий
Data Scientist
79 вакансий

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань