Содержание страницы
Ваш первый модуль расширения C API¶Your first C API extension module
Это руководство проведёт вас через создание простого модуля расширения Python, написанного на C или C++.
We will use the low-level Python C API directly. For easier ways to create extension modules, see the recommended third party tools.
The tutorial assumes basic knowledge about Python: you should be able to define functions in Python code before starting to write them in C. See The Python Tutorial for an introduction to Python itself.
Руководство должно быть доступно для любого, кто может написать простую библиотеку на C.
Хотя мы упомянем несколько концепций, которые новичок в C не обязан
знать, например функции static или объявления компоновки, понимание их
не обязательно для успеха.
Мы сосредоточимся на том, чтобы дать вам «ощущение» того, что такое C API Python. Оно не научит вас важным концепциям, таким как обработка ошибок и подсчёт ссылок, которые рассматриваются в следующих главах.
Мы предполагаем, что вы используете Unix-подобную систему (включая macOS и Linux) или Windows. В других системах вам, возможно, потребуется скорректировать некоторые детали – например, имя системной команды.
Вам потребуется установить подходящий компилятор C и заголовочные файлы Python для разработки.
В Linux заголовочные файлы часто находятся в пакете python3-dev
или python3-devel.
You need to be able to install Python packages.
This tutorial uses pip (pip install), but you
can substitute any tool that can build and install pyproject.toml-based
projects, like uv (uv pip install).
Preferably, have a virtual environment activated.
Примечание
В этом руководстве используются API, добавленные в CPython 3.15. Чтобы создать расширение, совместимое с более ранними версиями CPython, следуйте более ранней версии этой документации.
В этом руководстве используется синтаксис C, добавленный в C11 и C++20. Если ваше расширение должно быть совместимо с более ранними стандартами, следуйте руководствам в документации для Python 3.14 и ниже.
Что мы будем делать¶What we’ll do
Давайте создадим модуль расширения с именем spam [1],
который будет включать интерфейс Python к функции
стандартной библиотеки C system().
Эта функция определена в stdlib.h.
Она принимает строку C в качестве аргумента, выполняет аргумент как системную
команду и возвращает значение результата в виде целого числа.
Страница руководства для system() может резюмировать это так:
#include <stdlib.h>
int system(const char *command);
Обратите внимание, что, как и многие функции в стандартной библиотеке C, эта функция уже существует в Python. В реальных проектах используйте os.system() или subprocess.run() вместо модуля, который вы напишете здесь.
Эту функцию нужно вызывать из Python следующим образом:
>>> import spam
>>> status = spam.system("whoami")
User Name
>>> status
0
Примечание
Системная команда whoami выводит имя пользователя. Она полезна в таких учебных пособиях, потому что имеет одно и то же имя в Unix и Windows.
Начните с заголовков¶Start with the headers
Сначала создайте каталог для этого учебного пособия и перейдите в него в командной строке. Затем создайте файл с именем spammodule.c в вашем каталоге. [2]
В этом файле мы включим два заголовка: Python.h для всех объявлений C API Python и stdlib.h для функции system(). [3]
Добавьте следующие строки в spammodule.c:
#include <Python.h>
#include <stdlib.h> // для system()
Обязательно разместите stdlib.h и любые другие include-директивы стандартной библиотеки после Python.h. В некоторых системах Python может определять некоторые макросы препроцессора, которые влияют на стандартные заголовки.
Запуск вашей сборочной утилиты¶Running your build tool
Только с подключенными заголовками ваше расширение ничего не делает. Тем не менее, сейчас самое время скомпилировать его и попробовать импортировать. Это позволит убедиться, что сборочная утилита работает, чтобы вы могли вносить и тестировать изменения по мере чтения остального текста.
Сам CPython не включает инструмент для сборки модулей расширения; рекомендуется использовать сторонний проект. В этом учебном пособии мы будем использовать meson-python. (Если вы хотите использовать другой, см. Приложение: Другие сборочные утилиты.)
meson-python требует определения «проекта» с помощью двух дополнительных файлов.
Сначала добавьте pyproject.toml с таким содержимым:
[build-system]
build-backend = 'mesonpy'
requires = ['meson-python']
[project]
# Информация о проекте-заполнителе
# (измените это перед распространением модуля)
name = 'sampleproject'
version = '0'
Затем создайте meson.build, содержащий следующее:
project('sampleproject', 'c')
py = import('python').find_installation(pure: false)
py.extension_module(
'spam', # имя импортируемого модуля Python
'spammodule.c', # файл исходного кода C
install: true,
)
Примечание
Подробнее о настройках см. в документации meson-python.
Теперь выполните сборку и установку проекта в текущем каталоге (.) с помощью pip:
python -m pip -v install .
Опция -v (--verbose) заставляет pip показывать вывод компилятора, что часто бывает полезно во время разработки.
Совет
Если у вас не установлен pip, выполните python -m ensurepip, желательно в виртуальном окружении. (Или, если вы предпочитаете другой инструмент, который может собирать и устанавливать проекты на основе pyproject.toml, используйте его.)
Обратите внимание: эту команду нужно запускать заново каждый раз, когда вы изменяете расширение. В отличие от Python, в C есть явный этап компиляции.
Когда расширение скомпилировано и установлено, запустите Python и попробуйте импортировать его. Должна возникнуть следующая ошибка:
>>> import spam
Traceback (most recent call last):
...
ImportError: dynamic module does not define module export function (PyModExport_spam or PyInit_spam)
Хук экспорта модуля¶Module export hook
Исключение, которое вы получили при попытке импортировать модуль, сообщает, что Python ищет «функцию экспорта модуля», также известную как хук экспорта модуля. Давайте определим его.
Сначала добавьте прототип под строками #include:
PyMODEXPORT_FUNC PyModExport_spam(void);
Совет
Прототип не является строго обязательным, но некоторые современные компиляторы выдают предупреждения без него. Как правило, лучше добавить прототип, чем отключать предупреждение.
Макрос PyMODEXPORT_FUNC объявляет возвращаемый тип функции и добавляет все необходимые объявления связывания, чтобы функция была видима и доступна при загрузке CPython.
После прототипа добавьте саму функцию. Пока пусть она возвращает NULL:
PyMODEXPORT_FUNC
PyModExport_spam(void)
{
return NULL;
}
Скомпилируйте и загрузите модуль снова. На этот раз вы должны получить другую ошибку.
>>> import spam
Traceback (most recent call last):
...
SystemError: module export hook for module 'spam' failed without setting an exception
Просто возвращать NULL не является корректным поведением для хука экспорта, и CPython сообщает об этом. Это хорошо – значит, CPython нашёл функцию! Теперь сделаем её полезной.
Таблица слотов¶The slot table
Вместо NULL хук экспорта должен возвращать информацию, необходимую для создания модуля. Начнём с основ: имени и строки документации.
Информация должна быть определена в массиве записей PySlot, которые по сути являются парами ключ-значение. Определите этот массив прямо перед вашим экспортным хуком:
PyABIInfo_VAR(abi_info);
static PySlot spam_slots[] = {
PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
PySlot_STATIC_DATA(Py_mod_name, "spam"),
PySlot_STATIC_DATA(Py_mod_doc, "A wonderful module with an example function"),
PySlot_END
};
Макрос PySlot_STATIC_DATA используется, когда значение слота (здесь: &abi_info, "spam" и докстринг) является указателем на константные статически выделенные данные.
Макрос PyABIInfo_VAR(abi_info); и слот Py_mod_abi представляют собой шаблонный код, который помогает предотвратить аварию интерпретатора из-за расширений, скомпилированных для другой версии Python.
Для обоих Py_mod_name и Py_mod_doc значениями являются C-строки – то есть байтовые массивы в кодировке UTF-8, оканчивающиеся нулевым символом NUL.
Обратите внимание на сторожевую запись PySlot_END в конце. Она отмечает конец массива. При её отсутствии возникнет неопределённое поведение.
Массив определён как static – то есть невидим за пределами этого файла .c. Это станет общей практикой. CPython нужен доступ только к экспортному хуку; все глобальные переменные и остальные функции должны быть static, чтобы не конфликтовать с другими расширениями.
Верните этот массив из вашего экспортного хука вместо NULL:
PyMODEXPORT_FUNC
PyModExport_spam(void)
{
return spam_slots;
}
Теперь перекомпилируйте и проверьте:
>>> import spam
>>> print(spam)
<module 'spam' from '/home/encukou/dev/cpython/spam.so'>
У вас есть модуль расширения! Попробуйте help(spam), чтобы увидеть докстринг.
Следующим шагом будет добавление функции.
Предоставление функции¶Exposing a function
Чтобы предоставить C-функцию system() напрямую Python, потребуется написать слой связующего кода для преобразования аргументов из объектов Python в значения C и обратного преобразования возвращаемого значения C в Python.
Один из простейших способов написать связующий код – это функция «METH_O», которая принимает два объекта Python и возвращает один. Все объекты Python – независимо от типа – представлены в C как указатели на структуру PyObject.
Добавьте такую функцию над массивом слотов:
static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
Py_RETURN_NONE;
}
Пока мы игнорируем аргументы и используем макрос Py_RETURN_NONE, который разворачивается в оператор return, корректно возвращающий объект Python None.
Перекомпилируйте ваше расширение, чтобы убедиться, что нет синтаксических ошибок. Мы ещё не добавили spam_system в модуль, поэтому вы можете получить предупреждение, что spam_system не используется.
Определения методов¶Method definitions
Чтобы предоставить C-функцию Python, потребуется указать несколько фрагментов информации в структуре, называемой PyMethodDef [4]:
ml_name: имя функции Python;ml_doc: докстринг;ml_meth: вызываемая C-функция; иml_flags: набор флагов, описывающих детали, например, как аргументы Python передаются C-функции. Мы будем использоватьMETH_O– флаг, соответствующий сигнатуре нашей функцииspam_system.
Поскольку модули обычно создают несколько функций, эти определения нужно собрать в массив с нулевой сторожевой записью в конце. Добавьте этот массив сразу под функцией spam_system:
static PyMethodDef spam_methods[] = {
{
.ml_name="system",
.ml_meth=spam_system,
.ml_flags=METH_O,
.ml_doc="Execute a shell command.",
},
{NULL, NULL, 0, NULL} /* Страж */
};
Как и в случае со слотами модуля, нулевая сторожевая запись отмечает конец массива.
Далее мы добавим метод в модуль. Добавьте слот Py_mod_methods в ваш массив PyMethodDef:
PyABIInfo_VAR(abi_info);
static PySlot spam_slots[] = {
PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
PySlot_STATIC_DATA(Py_mod_name, "spam"),
PySlot_STATIC_DATA(Py_mod_doc, "A wonderful module with an example function"),
PySlot_STATIC_DATA(Py_mod_methods, spam_methods),
PySlot_END
};
Снова перекомпилируйте ваше расширение и протестируйте его. Обязательно перезапустите интерпретатор Python, чтобы import spam загрузил новую версию модуля.
Теперь вы должны иметь возможность вызвать функцию:
>>> import spam
>>> print(spam.system)
<built-in function system>
>>> print(spam.system('whoami'))
None
Обратите внимание, что наша spam.system пока не выполняет команду whoami; она только возвращает None.
Проверьте, что функция принимает ровно один аргумент, как указано флагом METH_O:
>>> print(spam.system('too', 'many', 'arguments'))
Traceback (most recent call last):
...
TypeError: spam.system() takes exactly one argument (3 given)
Возврат целого числа¶Returning an integer
Теперь давайте посмотрим на возвращаемое значение. Вместо None мы хотим, чтобы spam.system возвращала число – то есть объект Python int. В конечном итоге это будет код выхода системной команды, но начнём с фиксированного значения, скажем, 3.
Python C API предоставляет функцию для создания объекта Python int из значения C int: PyLong_FromLong(). [5]
Чтобы вызвать его, замените Py_RETURN_NONE следующими тремя строками:
static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
int status = 3;
PyObject *result = PyLong_FromLong(status);
return result;
}
Перекомпилируйте, снова перезапустите интерпретатор Python и проверьте, что функция теперь возвращает 3:
>>> import spam
>>> spam.system('whoami')
3
Принимает строку¶Accepting a string
Наконец, давайте обработаем аргумент функции.
Наша C-функция spam_system() принимает два аргумента.
Первый, PyObject *self, будет установлен на объект модуля spam.
В нашем случае это бесполезно, так что мы его проигнорируем.
Второй аргумент, PyObject *arg, будет установлен на объект, который пользователь
передал из Python.
Мы ожидаем, что это строка Python.
Чтобы использовать содержащуюся в ней информацию, нам нужно
преобразовать её в C-значение – в данном случае, C-строку (const char *).
Здесь есть небольшое несоответствие типов: объекты Python str хранят
текст Unicode, а C-строки – это массивы байтов.
Поэтому нам нужно закодировать данные, и мы будем использовать кодировку UTF-8.
(UTF-8 может не всегда подходить для системных команд, но это то, что
str.encode() использует по умолчанию,
и C API имеет специальную поддержку для неё.)
Функция для кодирования строки Python в буфер UTF-8 называется
PyUnicode_AsUTF8AndSize() [6].
Вызывайте её так:
static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
const char *command = PyUnicode_AsUTF8AndSize(arg, NULL);
int status = 3;
PyObject *result = PyLong_FromLong(status);
return result;
}
Если PyUnicode_AsUTF8AndSize() успешен, command будет указывать на
полученную C-строку – байтовый массив, завершающийся нулём [7].
Этот буфер управляется объектом arg, что означает, что нам не нужно освобождать
его, но мы должны соблюдать некоторые правила:
Мы должны использовать буфер только внутри функции
spam_system. После возвратаspam_systemобъект arg и управляемый им буфер могут быть собраны сборщиком мусора.Мы не должны изменять его. Именно поэтому мы используем
const.
Если PyUnicode_AsUTF8AndSize() был не успешен, он возвращает указатель NULL.
При вызове любого Python C API нам всегда нужно обрабатывать такие случаи ошибок.
Общий способ делать это будет рассмотрен в следующих главах этой документации.
Пока будьте уверены, что мы уже корректно обрабатываем ошибки от
PyLong_FromLong().
Для вызова PyUnicode_AsUTF8AndSize() правильный способ обработки
ошибок – вернуть NULL из spam_system.
Добавьте для этого блок if:
static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
const char *command = PyUnicode_AsUTF8AndSize(arg);
if (command == NULL) {
return NULL;
}
int status = 3;
PyObject *result = PyLong_FromLong(status);
return result;
}
Чтобы проверить, что обработка ошибок работает, снова скомпилируйте, перезапустите Python, чтобы
import spam подхватил новую версию вашего модуля, и попробуйте передать
в функцию значение, не являющееся строкой:
>>> import spam
>>> spam.system(3)
Traceback (most recent call last):
...
TypeError: bad argument type for built-in operation
Теперь осталось только вызвать функцию C-библиотеки system() с
буфером char * и использовать её результат вместо 3:
static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
const char *command = PyUnicode_AsUTF8AndSize(arg);
if (command == NULL) {
return NULL;
}
int status = system(command);
PyObject *result = PyLong_FromLong(status);
return result;
}
Скомпилируйте модуль, перезапустите Python и протестируйте.
На этот раз вы должны увидеть ваше имя пользователя – вывод системной команды whoami:
>>> import spam
>>> result = spam.system('whoami')
User Name
>>> result
0
Вы также можете протестировать с другими командами, например ls, dir или несуществующей:
>>> import spam
>>> result = spam.system('nonexistent-command')
sh: line 1: nonexistent-command: command not found
>>> result
32512
Результат¶The result
Поздравляем! Вы написали полноценный модуль расширения Python C API и завершили этот учебник!
Вот полный исходный файл для вашего удобства:
/// Включения
#include <Python.h>
#include <stdlib.h> // для system()
/// Реализация spam.system
static PyObject *
spam_system(PyObject *self, PyObject *arg)
{
const char *command = PyUnicode_AsUTF8AndSize(arg, NULL);
if (command == NULL) {
return NULL;
}
int status = system(command);
PyObject *result = PyLong_FromLong(status);
return result;
}
/// Таблица методов модуля
static PyMethodDef spam_methods[] = {
{
.ml_name="system",
.ml_meth=spam_system,
.ml_flags=METH_O,
.ml_doc="Execute a shell command.",
},
{NULL, NULL, 0, NULL} /* Страж */
};
/// Таблица слотов модуля
PyABIInfo_VAR(abi_info);
static PySlot spam_slots[] = {
PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
PySlot_STATIC_DATA(Py_mod_name, "spam"),
PySlot_STATIC_DATA(Py_mod_doc, "A wonderful module with an example function"),
PySlot_STATIC_DATA(Py_mod_methods, spam_methods),
PySlot_END
};
/// Прототип хука экспорта
PyMODEXPORT_FUNC PyModExport_spam(void);
/// Хук экспорта модуля
PyMODEXPORT_FUNC
PyModExport_spam(void)
{
return spam_slots;
}
Приложение: Другие инструменты сборки¶Appendix: Other build tools
Вы сможете следовать этому учебнику – за исключением самого раздела
Запуск вашего инструмента сборки – с другим инструментом сборки,
не meson-python.
Руководство пользователя по упаковке Python содержит список рекомендуемых инструментов; обязательно выберите тот, который подходит для языка C.
Обходное решение для отсутствующей функции PyInit¶Workaround for missing PyInit function
Если вывод вашего инструмента сборки сообщает об отсутствии PyInit_spam,
добавьте пока в свой модуль следующую функцию:
// Обходное решение
void *PyInit_spam(void) { return NULL; }
Это заглушка для функции инициализации старого образца, которая требовалась в модулях расширения для CPython 3.14 и ниже. Текущий CPython в ней не нуждается, но некоторые инструменты сборки могут по-прежнему предполагать, что все модули расширения должны её определять.
Если вы используете это обходное решение, вы получите исключение
SystemError: initialization of spam failed without raising an exception
вместо
ImportError: dynamic module does not define module export function.
Компиляция напрямую¶Compiling directly
Настоятельно рекомендуется использовать сторонний инструмент сборки, поскольку он позаботится о различных деталях вашей платформы и установки Python, именовании результирующего расширения и, позднее, о распространении вашей работы.
Если вы собираете расширение для конкретной системы или только для себя, вы, возможно, захотите запустить компилятор напрямую. Способ сделать это зависит от системы; будьте готовы к проблемам, которые придётся решать самостоятельно.
Linux¶
В Linux пакет разработки Python может включать команду python3-config,
которая выводит необходимые флаги компилятора.
Если вы её используете, проверьте, что она соответствует интерпретатору CPython, который вы будете использовать
для загрузки модуля.
Затем начните с следующей команды:
gcc --shared $(python3-config --cflags --ldflags) spammodule.c -o spam.so
Это должно создать файл spam.so, который нужно поместить в каталог
sys.path.
Сноски
Эта страница – перевод. Оригинал на английском – docs.python.org. Нашли неточность? Сообщите нам.