2025, Oct 19 07:18
Инициализация Cython-модуля при встраивании Python в C
Почему минимальный Hello World с Cython падает при встраивании Python в C и как это исправить: PyImport_AppendInittab, Py_Initialize и импорт модуля.
Связывать C и Python через Cython — обычная практика, но есть тонкий шаг инициализации, от которого зависит, заработает ли ваше встраивание. Минимальный «Hello World», печатающий из Python и вызываемый из программы на C, получит сегфолт, если модуль Cython не инициализировать и не импортировать заранее.
Постановка задачи
Рассмотрим крошечную функцию на Python, открытую через обёртку Cython и вызываемую из C. Цель — вывести «Hello World» из Python при том, что точка входа — C.
# greetmod.py
def say_hi():
  print("Hello World")
# bridge.pyx
from greetmod import say_hi
cdef public void invoke_hi():
  say_hi()
/* main.c */
#include <Python.h>
#include "bridge.h"
int
main()
{
  Py_Initialize();
  invoke_hi();
  Py_Finalize();
}
В таком виде запуск бинарника заканчивается аварией. Трассировка показывает сбой при попытке разрешить имя на уровне глобальных переменных модуля — что-то вроде сегфолта внутри вызова наподобие __Pyx__GetModuleGlobalName с нулевым указателем имени.
Что на самом деле происходит
Даже если обёрточная функция объявлена как public в Cython, сама обёртка остаётся модулем Python. Его инициализация на уровне Python должна выполниться до того, как можно использовать любой код или глобальные переменные. Это касается и строки from greetmod import say_hi. Если перескочить сразу к экспортируемой C‑функции, не импортировав модуль Cython, глобальное пространство имён модуля останется неинициализированным, и обращение к его глобалам приведёт к падению.
Правильная последовательность такова: зарегистрировать функцию инициализации модуля, запустить интерпретатор Python, убедиться, что Python видит ваши модули в sys.path, и затем импортировать модуль Cython. Лишь после этого безопасно вызывать публичную функцию.
Исправление и рабочий код
Поток встраивания включает три ключевых шага: добавить функцию инициализации модуля через PyImport_AppendInittab до старта интерпретатора, инициализировать Python вызовом Py_Initialize и импортировать модуль через PyImport_ImportModule, чтобы выполнилась его инициализация и импорты. Ещё важно, чтобы Python мог найти ваши модули: в сценариях встраивания текущая рабочая директория автоматически не попадает в sys.path.
/* main.c (исправлено) */
#include <Python.h>
#include <stdio.h>
#include "bridge.h"
int
main()
{
    PyObject *modref;
    /* Зарегистрируйте «bridge» как встроенный модуль до вызова Py_Initialize.
       PyInit_bridge сгенерирован Cython из bridge.pyx. */
    if (PyImport_AppendInittab("bridge", PyInit_bridge) == -1) {
        fprintf(stderr, "Error: could not extend in-built modules table\n");
        exit(1);
    }
    /* Запустите интерпретатор Python. */
    Py_Initialize();
    /* При необходимости настройте путь импорта, чтобы Python нашёл ваши модули. */
    // PyRun_SimpleString("import sys\nsys.path.insert(0,'')");
    /* Импортируйте модуль Cython, чтобы выполнились его инициализация и импорты. */
    modref = PyImport_ImportModule("bridge");
    if (!modref) {
        PyErr_Print();
        fprintf(stderr, "Error: could not import module 'bridge'\n");
        goto fail;
    }
    /* Теперь можно безопасно вызвать публичную функцию. */
    invoke_hi();
    Py_Finalize();
    return 0;
fail:
    Py_Finalize();
    return 1;
}
Если вам ближе настройка пути импорта через окружение, при запуске программы установите PYTHONPATH в текущую директорию — так интерпретатор найдёт greetmod.py и сгенерированный модуль:
PYTHONPATH=. ./main
Либо используйте встроенный вариант из примера выше: вставьте пустую строку в sys.path через PyRun_SimpleString после Py_Initialize и до первого импорта.
Почему это важно
Пропуск шага импорта — не безобидная оптимизация. Код на C, который генерирует Cython, рассчитан на импорт как модуль Python; именно импорт запускает инициализацию модуля, включая выполнение его верхнеуровневого Python‑кода. Если обойти инициализацию и вызывать экспортированные функции напрямую, падения весьма вероятны. Импорт можно отложить до момента, когда модуль действительно понадобится, но исключать его нельзя.
Важен момент, когда импортировать, а не вопрос, нужно ли импортировать. Модуль обязан быть импортирован: вы можете выбрать время, но не можете отказаться от импорта.
Итоги
Встраивая Python и вызывая сгенерированный Cython код из C, относитесь к своей обёртке как к обычному модулю Python: зарегистрируйте её через PyImport_AppendInittab до старта интерпретатора, инициализируйте Python, убедитесь, что в sys.path есть путь к модулю, импортируйте модуль, чтобы выполнилась его инициализация, и только затем вызывайте публичные функции. Эта небольшая процедура загрузки избавляет от трудноотлавливаемых сегфолтов и делает минимальный «Hello World» предсказуемым.