Содержание страницы
Миграция на стабильный ABI для свободной многопоточности (abi3t)¶Migrating to Stable ABI for free threading (abi3t)
Начиная с версии 3.15, CPython поддерживает вариант стабильного ABI,
который поддерживает со свободной многопоточностью Python:
стабильный ABI для сборок со свободной многопоточностью, или сокращённо abi3t.
В этом документе описывается, как адаптировать расширения C API для поддержки свободной многопоточности.
Зачем это нужно¶Why do this
Обычная причина использования стабильного ABI – сократить количество артефактов, которые необходимо собирать и распространять для каждой версии своей библиотеки.
Без стабильного ABI необходимо собирать отдельную разделяемую библиотеку и, как правило, дистрибутив wheel для каждой функциональной версии CPython, которую требуется поддерживать. Например, каждый тег в следующей таблице представляет отдельную библиотеку/wheel:
Версия CPython |
Без свободной многопоточности |
Со свободной многопоточностью |
|---|---|---|
3.12 |
|
– |
3.13 |
|
|
3.14 |
|
|
3.15 |
|
|
3.16 |
|
|
Более поздние версии |
|
|
Это много сборок, особенно если умножить на количество поддерживаемых платформ.
Благодаря стабильному ABI (abi3, представленному в CPython 3.2), одно расширение
(на платформу) может покрывать все сборки без свободной потоковости CPython:
Версия CPython |
Без свободной потоковости |
Со свободной потоковостью |
|---|---|---|
3.12 |
|
– |
3.13 |
|
|
3.14 |
|
|
3.15 |
|
|
3.16 |
|
|
Более поздние версии |
|
Стабильный ABI для сборок со свободной потоковостью (abi3t), представленный в
CPython 3.15, делает то же самое для сборок со свободной потоковостью.
И он также совместим со сборками без свободной потоковости:
Версия CPython |
Без свободной потоковости |
Со свободной потоковостью |
|---|---|---|
3.12 |
|
– |
3.13 |
|
|
3.14 |
|
|
3.15 |
|
|
3.16 |
||
Более поздние версии |
||
* (Как и выше, расширение abi3 совместимо со всеми сборками без свободной потоковости;
даже с теми версиями 3.15+, которые эта таблица «приписывает» abi3t.)
Почему бы не сделать это¶Why not do this
У стабильного ABI есть два основных недостатка.
Во-первых, ваше расширение может работать медленнее, так как стабильный ABI ставит совместимость выше производительности. Разница обычно незаметна, и её часто можно смягчить, используя один и тот же исходный код для сборки как со стабильным ABI, так и нескольких версионно-специфичных сборок для «основных» версий CPython.
Во-вторых, доступна не вся часть C API. Расширения необходимо портировать для сборки со стабильным ABI, что может быть сложно или, в редких случаях, невозможно.
В частности, abi3t требует API, добавленных в CPython 3.15.
Если вы хотите собирать расширение для более старых версий CPython из
одного исходного кода, у вас есть два основных варианта:
Используйте условные конструкции препроцессора.
При следовании этому руководству используйте блоки
#ifdef Py_TARGET_ABI3Tвсякий раз, когда вам говорят внести изменение, которое нарушает сборку на интересующих вас версиях CPython. Оставляйте существующий код в блоках#else.Для написанных вручную расширений C такой подход разумен вплоть до CPython 3.12, благодаря дополнениям, введённым в PEP 697. Сохранение совместимости с 3.11 и ниже может быть оправдано для генераторов кода (например, Cython).
Не портируйте на
abi3tи продолжайте собирать отдельные расширения для каждой версии CPython, пока не сможете отказаться от поддержки старых версий.Это допустимый подход. Не всем расширениям нужно переходить на
abi3tпрямо сейчас.
Предварительные требования¶Prerequisites
В этом руководстве предполагается, что у вас есть расширение, написанное непосредственно на C (или C++),
которое вы хотите портировать на abi3t.
Если ваше расширение использует генератор кода (например, Cython) или языковую привязку
(например, PyO3), лучше подождать, пока этот инструмент не получит поддержку abi3t.
Если вы поддерживаете такой инструмент, возможно, вы сможете адаптировать инструкции
из этого руководства для своего инструмента.
Стабильный ABI без свободной потоковости¶Non-free-threaded Stable ABI
Ваше расширение должно поддерживать стабильный ABI (abi3t).
Если нет, либо сначала портируйте его, либо следуйте этому руководству, но будьте готовы исправить
проблемы, которые в нём не упомянуты.
Поддержка свободной потоковости¶Free-threading support
Хотя технически это не строгое требование, скорее всего, вы захотите
подготовить расширение для свободной потоковости, прежде чем портировать его на abi3t.
См. Поддержка расширений C API для свободной потоковости за инструкциями.
См. также
Портирование модулей расширений для поддержки свободной потоковости: Руководство по портированию, поддерживаемое сообществом, для авторов расширений.
Изоляция модулей расширений¶Isolating extension modules
Ваш модуль должен использовать многофазную инициализацию, и должен быть либо изолированным, либо ограничиваться загрузкой не более одного раза на процесс. Если это не ваш случай, сначала следуйте разделу «Изоляция расширений модулей». (См. раздел об отказе для быстрого решения.)
Избегание типов переменного размера¶Avoiding variable-sized types
Если ваше расширение определяет типы переменного размера (с помощью Py_tp_itemsize
или PyTypeObject.tp_itemsize), его невозможно портировать на
abi3t 3.15.
Настройка сборки¶Setting up the build
Если вы используете инструмент сборки (например, setuptools, meson-python, scikit-build-core),
найдите в его документации способ выбрать abi3t.
На момент написания не все они это поддерживают; но если ваш инструмент поддерживает,
используйте его.
Возможно, вам стоит проверить, что он установил правильный флаг, временно добавив
следующий код сразу после #include <Python.h>:
#if Py_TARGET_ABI3T+0 <= 0x30f0000
#error "abi3t define is not set!"
#endif
Это должно привести к другой ошибке, нежели «abt3t define is not set».
Примечание
Если ваш инструмент сборки ещё не поддерживает abi3t, установите следующий макрос
перед включением Python.h:
#define Py_TARGET_ABI3T 0x30f0000
или укажите его как флаг компилятора, например:
-DPy_TARGET_ABI3T=0x30f0000
Как только ваше расширение будет собираться с этой настройкой, оно станет совместимым с CPython 3.15 и выше.
Если вы устанавливаете этот макрос вручную, вам также потребуется вручную назвать и пометить полученное расширение. Это описано в разделе «Пометка и распространение» ниже.
В этом руководстве вам будет предложено выполнить ряд изменений.
После каждого изменения проверяйте, что ваше расширение по-прежнему собирается в исходной
(не-abi3t) конфигурации, и желательно запускайте тесты на всех поддерживаемых
вами версиях Python.
Это гарантирует, что в процессе портирования ничего не сломается.
Хук экспорта модуля¶Module export hook
Если вы ещё не выполнили этот шаг, ваш модуль расширения определяет
функцию инициализации модуля
с именем PyInit_<module_name>.
Вам нужно будет перенести её в хук экспорта модуля,
PyModExport_<module name>, функцию, добавленную в CPython 3.15 в
PEP 793.
Ваша существующая функция init должна выглядеть примерно так (с вашими собственными именами
для <modname> и <moddef>):
PyMODINIT_FUNC
PyInit_<modname>(void)
{
return PyModuleDef_Init(&<moddef>);
}
Если перед return есть какой-то код, переместите его в
слот-функцию Py_mod_create или Py_mod_exec.
См. документацию PyInit для получения дополнительной информации.
Функция ссылается на объект PyModuleDef (<moddef> в коде
выше).
Его определение должно быть похоже на следующее, с другими значениями
и, возможно, некоторыми неименованными или пропущенными полями:
static PyModuleDef <moddef> = {
PyModuleDef_HEAD_INIT,
.m_name = "my_module",
.m_doc = "my docstring",
.m_size = sizeof(my_state_struct),
.m_methods = my_methods,
.m_slots = my_slots,
.m_traverse = my_traverse,
.m_clear = my_clear,
.m_free = my_free,
};
Удалите это определение и функцию PyInit (или поместите их в
блок #ifndef Py_TARGET_ABI3T для сохранения обратной совместимости),
и замените их следующим:
PyABIInfo_VAR(abi_info);
static PySlot my_slot_array[] = {
PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
PySlot_STATIC_DATA(Py_mod_name, "my_module"),
PySlot_STATIC_DATA(Py_mod_doc, "my docstring"),
PySlot_SIZE(Py_mod_state_size, sizeof(my_state_struct)),
PySlot_STATIC_DATA(Py_mod_methods, my_methods),
PySlot_STATIC_DATA(Py_mod_slots, my_slots),
PySlot_FUNC(Py_mod_state_traverse, my_traverse),
PySlot_FUNC(Py_mod_state_clear, my_clear),
PySlot_FUNC(Py_mod_state_free, my_free),
PySlot_END
};
PyMODEXPORT_FUNC
PyModExport_<modname>(void)
{
return my_slot_array;
}
Опустите любые поля, которые отсутствовали (кроме нового Py_mod_abi),
и подставьте свои собственные значения.
См. документацию PySlot и хука экспорта
для получения подробностей об этом API.
Как в примере, ваша функция PyModExport_ должна только возвращать
указатель на статические данные.
Если вы не можете избежать дополнительного кода, обратитесь к
предостережениям в документации PyModExport.
Существующие слоты¶Existing slots
Если у вас есть слот Py_mod_slots, проверьте массив, на который он ссылается.
Это должен быть массив PyModuleDef_Slot, например:
static PyObject *create_module(PyObject *spec, PyModuleDef *def) { ... }
static int my_first_module_exec(PyObject *module) { ... }
static int my_second_module_exec(PyObject *module) { ... }
static PyModuleDef_Slot my_slots[] = {
{Py_mod_gil, Py_MOD_GIL_NOT_USED},
{Py_mod_multiple_interpreters, Py_MOD_PER_INTERPRETER_GIL_SUPPORTED},
{Py_mod_create, my_module_create},
{Py_mod_exec, my_first_module_exec},
{Py_mod_exec, my_second_module_exec},
{0, NULL}
};
py_mod_create¶
Если у вас есть запись Py_mod_create, убедитесь, что функцию можно
вызвать с NULL в качестве второго аргумента (вместо
PyModuleDef, который вы удаляете).
Часто этот аргумент вообще не используется; вы можете проверить, переименовав его:
static PyObject *create_module(PyObject *spec, PyModuleDef *_unused) { ... }
Если аргумент используется, найдите другой способ передачи данных. Обычно информация статична, и вы можете обращаться к ней напрямую. (Если вы используете одну функцию для нескольких разных модулей, рассмотрите возможность определить несколько функций вместо этого.)
Несколько py_mod_exec¶Multiple py_mod_exec
Если у вас есть более одной записи Py_mod_exec, объедините их:
создайте новую функцию, которая вызывает остальные, и замените ею существующие слоты.
static int my_module_exec(PyObject *module) {
if (my_first_module_exec(module) < 0) return -1;
if (my_second_module_exec(module) < 0) return -1;
}
static PyModuleDef_Slot my_slots[] = {
...
/* удалить остальные слоты Py_mod_exec */
...
{Py_mod_exec, my_module_exec},
{0, NULL}
};
Если функции нигде больше не используются, вы можете вместо этого объединить их тела.
Объединение массивов слотов¶Merging slot arrays
Опционально, когда вы нарушаете совместимость с Python 3.14, вы можете очистить
код, переместив слоты в массив PySlot и преобразовав
определения в PySlot_DATA и PySlot_FUNC:
static PySlot my_slot_array[] = {
...
PySlot_DATA(Py_mod_gil, Py_MOD_GIL_NOT_USED),
PySlot_DATA(Py_mod_multiple_interpreters,
Py_MOD_PER_INTERPRETER_GIL_SUPPORTED)
PySlot_FUNC(Py_mod_create, my_module_create),
PySlot_FUNC(Py_mod_exec, my_module_exec),
PySlot_END
};
Если вы это сделаете, удалите исходный массив PyModuleDef_Slot и
его запись Py_mod_slots.
Связанные PyModuleDef¶Associated PyModuleDef
Поскольку новый API не использует структуру PyModuleDef, определение
не будет связано с результирующим модулем.
Это изменяет поведение следующих функций:
Проверьте свой код на наличие этих функций. Если вы их не используете, можете пропустить этот раздел.
Обычно эти функции применяются для двух целей:
Получить определение, с которым был создан модуль. В новом API это больше невозможно. Модули больше не хранят ссылку на определение, поэтому придётся найти другой способ передавать нужные данные.
Проверить, «ваш» ли данный объект модуля. Теперь эту задачу решают токены модулей – непрозрачные указатели, идентифицирующие модуль. Чтобы использовать токен, объявите (или переиспользуйте) уникальную статическую переменную, например:
static char my_token;
и добавьте указатель на неё в новую запись массива
PySlotвашего модуля:static PySlot my_slot_array[] = { ... PySlot_STATIC_DATA(Py_mod_token, &my_token), PySlot_END }
Затем перейдите от вызовов
PyModule_GetDef()вида:PyModuleDef *def = PyModule_GetDef(module);
к
PyModule_GetToken()(которая использует выходной аргумент и может завершиться исключением):void *token; if (PyModule_GetToken(module, &token) < 0) { /* обработать ошибку */ }
и от вызовов
PyType_GetModuleByDef()вида:PyObject *module = PyType_GetModuleByDef(type, my_def); /* обработать ошибку; использовать модуль */
к
PyType_GetModuleByToken()(которая возвращает сильную ссылку):PyObject *module = PyType_GetModuleByToken(type, my_token); /* обработать ошибку; использовать модуль */ Py_XDECREF(module);
PyObject непрозрачность¶PyObject opaqueness
Структуры PyObject и PyVarObject являются непрозрачными в abi3t.
Обращаться к их членам запрещено. Если вы это делали, переключитесь на функции-геттеры/сеттеры, упомянутые в их документации:
Кроме того, компилятору неизвестен размер структур PyObject.
Он может – и действительно – изменяться между разными сборками CPython.
Примечание
Хотя размер доступен во время выполнения (например, как sys.getsizeof(object()) в коде Python), не стоит поддаваться искушению вычислять по нему смещения указателей.
Расположение полей объекта может измениться в будущих реализациях abi3t.
Определения пользовательских типов¶Custom type definitions
Поскольку PyObject непрозрачен, традиционный способ определения пользовательских типов больше не работает:
typedef struct {
PyObject_HEAD // раскрывается в `PyObject ob_base;` с неизвестным размером
int my_data;
} CustomObject;
static PyType_Spec CustomType_spec = {
...
.basicsize = sizeof(CustomObject),
...
};
Скорее всего, все определения ваших классов и весь код, обращающийся к данным этих классов, нужно будет переписать.
Вероятно, это самое большое изменение, которое потребуется для поддержки abi3t.
Для каждого такого типа вместо определения struct для всего экземпляра определите его только с «дополнительными» полями – теми, что относятся к вашему классу, а не к его суперклассам:
typedef struct {
int my_data;
} CustomObjectData;
Измените имя.
Почти весь код, использующий эту структуру, нужно будет изменить (в частности, указатели на новую структуру нельзя просто приводить к PyObject* и обратно),
а смена имени позволит подсветить все места использования как ошибки компилятора.
(Если вы используете typeof, C++ auto или подобные способы, чтобы не писать имя типа, это не сработает. Будьте особенно внимательны и рассмотрите запуск инструментов для поиска неопределённого поведения.)
Затем для создания класса используйте отрицательное значение basicsize, чтобы указать «дополнительное» пространство для хранения, а не общий размер экземпляра:
static PyType_Spec CustomType_spec = {
...
.basicsize = -sizeof(CustomObjectData), /* обратите внимание на знак минус */
...
};
Если вы используете Py_tp_members, установите флаг Py_RELATIVE_OFFSET для каждого члена и укажите offset относительно вашей новой структуры.
Доступ к данным пользовательского типа¶Custom type data access
Далее начинается самое сложное: во всём коде, которому нужен доступ к этой структуре, потребуется дополнительный вызов PyObject_GetTypeData() для получения указателя CustomObjectData * из PyObject *:
PyObject *obj = ...;
CustomObjectData *data = PyObject_GetTypeData(obj, cls);
Обратите внимание: этот вызов требует объект типа вашего класса (cls).
Если ваш класс не допускает наследования (то есть не использует флаг Py_TPFLAGS_BASETYPE), то cls будет Py_TYPE(obj).
В противном случае НЕ ИСПОЛЬЗУЙТЕ Py_TYPE с PyObject_GetTypeData(): он может вернуть память, зарезервированную для постороннего подкласса!
Например, если пользователь создаст такой подкласс:
class Sub(YourCustomClass):
__slots__ = ('a', 'b')
то Py_TYPE(obj) равно Sub, и лежащая в основе память может выглядеть так:
╭─ PyObject *obj
│ ╭─ the pointer you want
│ │ ╭─ PyObject_GetTypeData(obj, Py_TYPE(obj))
▼ ▼ ▼
┌──────────┬───┬────────────────┬───┬─────────────┬───┬─────────────┐
│ PyObject │...│ CustomTypeData │...│ PyObject *a │...│ PyObject *b │
└──────────┴───┴────────────────┴───┴─────────────┴───┴─────────────┘
(Многоточия обозначают возможное выравнивание. Обратите внимание: такое расположение в памяти не гарантируется – будущие версии Python могут добавить другое выравнивание или даже изменить порядок структур.)
Есть два основных способа получить правильный класс:
В методах экземпляра ваша реализация может использовать сигнатуру
PyCMethod(и битMETH_METHODвPyMethodDef.ml_flags), получая класс как аргументdefining_class.В противном случае дайте вашему классу уникальный статический токен через слот
Py_tp_tokenи используйте:PyTypeObject cls; if (PyType_GetBaseByToken(Py_TYPE(obj), my_tp_token, &cls) < 0) { /* обработать ошибку */ } CustomObjectData *data = PyObject_GetTypeData(obj, cls);
Токены типов работают так же, как токены модулей, описанные ранее в этом руководстве.
Избегайте условной компиляции¶Avoid build-time conditionals
Проверьте свой код на наличие API, определяющего версию Python, под которую собран ваш модуль расширения. Больше это не соответствует той версии Python, на которой модуль выполняется, поэтому такой код часто требует изменений. Ищите следующие макросы:
PY_VERSION_HEX,PY_MAJOR_VERSION,PY_MINOR_VERSION:Чтобы получить версию времени выполнения, используйте
Py_Version;Чтобы определить, какой C API доступен, используйте
Py_TARGET_ABI3T. Этот макрос устанавливается в минимальную поддерживаемую версию.
Py_GIL_DISABLED: вabi3tэтот макрос всегда определён. Код, работающий со свободно-поточной версией Python, должен также работать и с включённым GIL (поскольку GIL может быть включён во время выполнения), и обычно работает (если только по какой-то причине ему не требуется более одного прикреплённого состояния потока одновременно).
Дальнейшие изменения кода¶Further code changes
Если у вас всё ещё остаются ошибки или предупреждения компилятора, найдите способ их исправить. Увы, это руководство ограничено и не может охватить все возможные изменения кода, которые могут потребоваться расширениям.
Если вы столкнулись с проблемой, с которой могут встретиться другие авторы расширений, рассмотрите возможность сообщить о проблеме (или отправить pull request) для этого руководства.
Возможно, вашу проблему невозможно исправить в текущей версии abi3t.
В таком случае сообщение о ней может помочь расставить приоритеты для следующей версии CPython.
Тегирование и распространение¶Tagging and distribution
Если вы используете инструмент сборки с поддержкой abi3t, ваше расширение готово,
но вы, возможно, захотите проверить, что оно собрано правильно.
Расширения, собранные с abi3t, должны иметь следующее расширение:
В Windows:
.pyd(как и любое другое расширение);Linux, macOS и другие системы, использующие суффикс
.so:.abi3t.so(не.cpython-315t.soили.abi3.so). Обратите внимание, что как свободно-поточные, так и несвободно-поточные сборки будут загружать расширения.abi3t.so;Другие системы: обратитесь к своему дистрибьютору и, возможно, обновите это руководство.
Если вы распространяете расширение в виде wheel, используйте следующие теги:
Тег Python:
cp3XX, где XX – минимальная версия Python, для которой собрано расширение. (Например,cp315, если вы установилиPy_TARGET_ABI3Tв0x30f0000. См. Компиляция для стабильного ABI для других значений.)Тег ABI:
abi3.abi3t. Это сжатый набор тегов, который указывает на поддержку как несвободно-поточных, так и свободно-поточных сборок.
Например, имя файла wheel может выглядеть так:
myproject-1.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl
См. также
Теги совместимости платформ в метаданных распространения пакетов PyPA.
Если имя файла или теги неверны, исправьте их.
Тестирование¶Testing
Обратите внимание: при сборке расширения, совместимого с несколькими версиями CPython, следует всегда тестировать его с каждой поддерживаемой версией (например, 3.15, 3.16 и так далее). Стабильный ABI гарантирует только ABI-совместимость; возможны также изменения поведения – как намеренные (описанные в PEP 387), так и ошибки.
Обязательно запускайте тесты как на свободно-поточной, так и на несвободно-поточной сборках CPython.
Если они проходят, поздравляем! У вас есть расширение abi3t.