ARTICLE DETAIL

建站实战干货

来自一线的建站与推广经验沉淀,每一条都经过真实交付验证。

CPython abi3t 迁移指南:将 C 扩展移植到支持 Free Threading 的 Stable ABI

2026/9/7 5:23:45 拓冰建站 浏览量
CPython abi3t 迁移指南:将 C 扩展移植到支持 Free Threading 的 Stable ABI CPython abi3t 迁移指南将 C 扩展移植到支持 Free Threading 的 Stable ABI【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本篇基于 CPython 官方文档 Doc/howto/abi3t-migration.rst 编写完整讲解自 3.15 版引入的 Free-Threaded Stable ABIabi3t迁移流程如何设置构建目标宏、把PyInit_初始化函数改写为 PEP 793 的PyModExport_导出钩子、应对PyObject变不透明opaque后的自定义类型重定义与数据访问以及最终的 wheel 标签与分发规范。读完并结合仓库源码佐证你可以将一个已支持abi3的 C/C 扩展模块完整移植为一份同时兼容 Free-Threaded 与非 Free-Threaded 构建的abi3t扩展。为什么迁移到 abi3t使用 Stable ABI 的典型动机是减少每个 CPython 版本需要构建和分发的产物数量。不使用 Stable ABI 时你必须在每个想支持的功能版本上分别构建一个共享库、以及一个对应的wheel分发包。下表每个标签都代表一个独立的库/wheelCPython 版本非 Free-ThreadedFree-Threaded3.12cpython-312—3.13cpython-313cpython-313t3.14cpython-314cpython-314t3.15cpython-315cpython-315t3.16cpython-316cpython-316t更后续版本cpython-3{XX}cpython-3{XX}t构建数量相当可观再乘以支持的平台数就更可观了。而 Stable ABIabi3CPython 3.2 引入可以用每平台一个扩展覆盖所有非 Free-Threaded构建CPython 版本非 Free-ThreadedFree-Threaded3.12abi3—3.13同一abi3cpython-313t3.14同一abi3cpython-314t3.15同一abi3cpython-315t3.16同一abi3cpython-316t更后续版本同一abi3cpython-3{XX}tFree-Threaded 构建的 Stable ABIabi3tCPython 3.15 引入对 Free-Threaded 构建做了同样的事——并且向下兼容非 Free-Threaded 构建CPython 版本非 Free-ThreadedFree-Threaded3.12abi3*—3.13同一产物cpython-313t3.14同一产物cpython-314t3.15 及以后abi3t同时覆盖两种构建同一产物*同上abi3扩展兼容所有非 Free-Threaded 构建包括本表归给abi3t的 3.15 版本。什么时候不该这么做Stable ABI 有两个主要缺点扩展可能变慢因为 Stable ABI 优先考虑兼容性而非性能。差异通常不可察觉且可以缓解用同一份源码既构建 Stable ABI 版本也针对一级CPython 版本构建少量版本专属版本。并非所有 C API 都可用。扩展需要移植才能构建为 Stable ABI这可能很难甚至在极少数情况下不可能。具体而言abi3t需要 CPython 3.15 新增的 API。如果你希望从同一份源码构建出更老 CPython 版本的扩展主要有两个选项使用预处理器条件编译。按本指南操作时每当某处修改会破坏你所关心的 CPython 版本上的构建就用#ifdef Py_TARGET_ABI3T代码块包裹新逻辑把原有代码保留在#else块中。对于手写 C 扩展得益于 PEP 697 引入的 API 补充这种方式向下兼容到 CPython 3.12 都还合理对代码生成器例如 Cython而言保持与 3.11 及更早版本的兼容也许可行。不移植到abi3t继续为每个 CPython 版本构建独立扩展直到你能放弃对旧版本的支持。这是一个合理的选择并非所有扩展都需要立刻切换。前置条件本指南假设你有一个直接以 C或 C编写的扩展希望将其移植到abi3t。如果你的扩展使用代码生成器如 Cython或语言绑定如 PyO3最好等该工具支持abi3t如果你是这类工具的维护者可以尝试把本指南的说明适配到你的工具中。非 Free-Threaded 的 Stable ABI你的扩展应当已经支持非 Free-Threaded Stable ABIabi3。若尚未支持要么先完成那一步移植要么按本指南操作但准备好修复文档未提及的问题。Free-Threading 支持虽然这不是硬性前置条件但你大概率会在移植abi3t之前先把扩展适配 Free Threading。可参见仓库中的 Doc/howto/isolating-extensions.rst 等相关 how-to原文档指向freethreading-extensions-howto一节。扩展模块的隔离你的模块应当使用多阶段初始化multi-phase initialization并且要么已经完成隔离isolated要么保证每个进程最多加载一次。若不是这种情况先按隔离扩展的指南操作其中还有 opt-out 快捷方式一节。避免可变大小类型如果你的扩展定义了可变大小的类型使用Py_tp_itemsize或PyTypeObject.tp_itemsize则无法移植到 3.15 的abi3t。配置构建如果使用构建工具setuptools、meson-python、scikit-build-core 等搜索其文档中如何选择abi3t。文档撰写时尚不是所有工具都支持如果你的工具支持就直接用。你可以临时在#include Python.h之后加入以下代码来验证工具是否设置了正确的标志#if Py_TARGET_ABI3T0 0x30f0000 #error abi3t define is not set! #endif如果未设置这里应当报出一个不同于 abi3tdefine is not set 的报错即Py_TARGET_ABI3T未定义导致预处理失败。注意如果你的构建工具尚不支持abi3t在包含Python.h之前定义如下宏#define Py_TARGET_ABI3T 0x30f0000或者作为编译器标志传入例如-DPy_TARGET_ABI3T0x30f0000。一旦扩展在此设置下构建通过它就与 CPython 3.15 及以后版本兼容。若手动设置该宏之后还需要手动命名和打标签产物见 标签与分发 一节。关于这个宏的底层语义可以看仓库头文件 Include/pyabi.hPy_TARGET_ABI3T的合法值必须 0x030f00003.15否则直接#errorpyabi.h 第 42–44 行它也可以由Py_LIMITED_API与Py_GIL_DISABLED同时定义隐式推导出第 61–64 行定义后会设置内部宏_Py_OPAQUE_PYOBJECT正是它让PyObject变不透明、把Py_LIMITED_API收敛到较低的值并在未定义时补上Py_GIL_DISABLED第 65–87 行。也就是说abi3t从预处理层面就强制不透明 PyObject Limited API Free-Threaded 假设三件套同时生效。本指南会要求你做一系列修改。每完成一步都要确认扩展在原有非abi3t配置下仍然能构建理想情况下在你支持的所有 Python 版本上跑一遍测试确保移植过程中没有东西被破坏。模块导出钩子Module Export Hook除非你已经完成这一步你的扩展模块会定义一个名为PyInit_module_name的模块初始化函数。需要把它移植到 CPython 3.15 中由 PEP 793 新增的模块导出钩子PyModExport_module_name。该 API 的完整说明可参见 Doc/c-api/extension-modules.rst 中的 extension-export-hook 小节。现有的 init 函数大致长这样用你自己的modname和moddef替换PyMODINIT_FUNC PyInit_modname(void) { return PyModuleDef_Init(moddef); }如果return之前有代码把它们移到Py_mod_create或Py_mod_exec槽函数里。该函数引用了一个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类型与相关宏可在 Include/slots.h 中找到PySlot是一个紧凑的静态描述结构sl_idsl_flags 联合体值宏PySlot_DATA/PySlot_FUNC/PySlot_SIZE/PySlot_STATIC_DATA/PySlot_END分别对应整型/指针值、函数指针、尺寸、静态指针与数组终止标记。而PyABIInfo_VAR宏在 Include/modsupport.h 中展开为一个静态的PyABIInfo变量由_PyABIInfo_DEFAULT填上PyABIInfo_STABLE | PyABIInfo_FREETHREADING_AGNOSTIC等标志这正是abi3t模块既支持 GIL 也支持 Free-Threaded的身份声明。PyMODEXPORT_FUNC在 Include/exports.h 中定义为_PyINIT_FUNC_DECLSPEC PySlot*——与PyMODINIT_FUNC相同的导出/链接属性但返回类型换成了槽数组指针。和示例一致你的PyModExport_函数只能返回指向静态数据的指针。如果实在无法避免额外代码参见PyModExport文档中的注意事项caveats一节。处理已有的 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如果你有多个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} };如果这些函数没有在别处使用也可以直接合并函数体。合并槽数组可选当你准备放弃与 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由于新 API 不再使用PyModuleDef结构不会有任何定义definition与最终创建的模块相关联。这会改变以下函数的行为PyModule_GetDefPyType_GetModuleByDef检查你的代码是否使用了它们如果没有可跳过本节。这些函数通常用于两个目的获取模块创建时使用的定义。使用新 API 后这不再可能。模块不再持有对定义的引用你需要想别的办法传递相关数据。判断某个模块对象是否是你的。这个用例现在由模块 tokenmodule token承担——一个标识模块的不透明指针。使用 token 时声明或复用一个唯一的静态变量例如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带输出参数、可能以异常失败声明见 Include/moduleobject.hvoid *token; if (PyModule_GetToken(module, token) 0) { /* 处理错误 */ }把PyType_GetModuleByDef的调用PyObject *module PyType_GetModuleByDef(type, my_def); /* 处理错误使用 module */换成PyType_GetModuleByToken返回强引用声明见 Include/object.hPyObject *module PyType_GetModuleByToken(type, my_token); /* 处理错误使用 module */ Py_XDECREF(module);PyObject 的不透明化Opaqueness在abi3t中PyObject与PyVarObject结构变为不透明opaque——这正是 Include/pyabi.h 中_Py_OPAQUE_PYOBJECT的作用。访问它们的成员是被禁止的。如果你正在这样做请改用其文档中提到的 getter/setter 函数来访问PyObject.ob_typePyObject.ob_refcntPyVarObject.ob_size此外PyObject结构体对编译器来说大小未知——它确实会在不同 CPython 构建之间变化。注意虽然大小在运行时可知例如 Python 代码中的sys.getsizeof(object())你应当克制住从它推算指针偏移的冲动。对象的内存布局在将来abi3t实现中可能改变。自定义类型定义由于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、Cauto之类手段避免写类型名这招就不灵了——要格外小心并考虑运行未定义行为检测工具。然后创建类时使用负数basicsize来表示额外存储空间而非整个实例大小static PyType_Spec CustomType_spec { ... .basicsize -sizeof(CustomObjectData), /* 注意负号 */ ... };如果你使用Py_tp_members给每个成员设置Py_RELATIVE_OFFSET标志并把PyMemberDef.offset指定为相对于新结构体的偏移。访问自定义类型数据接着是难的部分所有需要访问这个结构体的代码都要额外调用PyObject_GetTypeData声明见 Include/object.h从PyObject *取回CustomObjectData *指针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 │ ╭─ 你想要的指针 │ │ ╭─ PyObject_GetTypeData(obj, Py_TYPE(obj)) ▼ ▼ ▼ ┌──────────┬───┬────────────────┬───┬─────────────┬───┬─────────────┐ │ PyObject │...│ CustomTypeData │...│ PyObject *a │...│ PyObject *b │ └──────────┴───┴────────────────┴───┴─────────────┴───┴─────────────┘省略号表示可能存在填充。注意此内存布局不作保证未来版本可能加入不同的填充甚至改变结构的排列顺序。获取正确类对象有两种主要方式在实例方法中你的实现可以使用PyCMethod签名配合PyMethodDef.ml_flags中的METH_METHOD位并从defining_class参数得到类对象。其他情况用Py_tp_token槽给你的类设置一个唯一静态 token然后使用PyType_GetBaseByTokenPyTypeObject cls; if (PyType_GetBaseByToken(Py_TYPE(obj), my_tp_token, cls) 0) { /* 处理错误 */ } CustomObjectData *data PyObject_GetTypeData(obj, cls);类型 token 的用法与本指南前面介绍的模块 token 类似。避免构建期条件判断检查代码中所有用构建扩展时的 Python 版本来做判断的 API。在abi3t下构建版本不再等于扩展运行时的 Python 版本依赖该信息的代码通常都要改。需要检查的宏PY_VERSION_HEX、PY_MAJOR_VERSION、PY_MINOR_VERSION获取运行时版本用Py_Version判断可用哪些 C API用Py_TARGET_ABI3T。该宏被设置为你支持的最小版本。Py_GIL_DISABLED在abi3t下该宏恒被定义。能配合 Free-Threaded Python 工作的代码应当也配合 GIL 启用的构建工作因为 GIL 可以在运行时启用实际上通常确实如此除非代码因某种原因需要同时持有多个 attached thread state。一个值得注意的实现细节仓库的测试扩展test_cext在abi3t编译时会借助 GCC 的pragma GCC poison把Py_GIL_DISABLED毒化使任何对其做#if判断的代码在预处理阶段直接报错——见 Include/pyabi.h。这从机制上强制了abi3t 模块的 Python.h 内容不得依赖Py_GIL_DISABLED这一约束。其余代码修改如果仍然有编译错误或警告想办法修复它们。遗憾的是本指南篇幅有限无法覆盖扩展可能需要的一切代码变更。如果你发现其他扩展作者也可能遇到的问题考虑为这份指南提交 issue或 PR。你的问题可能无法在当前abi3t版本下修复即便如此报告它也有助于在 CPython 的下一个版本中优先处理。标签与分发如果使用支持abi3t的构建工具你的扩展已经就绪但建议确认构建正确。abi3t构建的扩展应当具有以下扩展名Windows.pyd与任何其他扩展一样Linux、macOS 及其他使用.so后缀的系统.abi3t.so不是.cpython-315t.so也不是.abi3.so。注意 Free-Threaded 与非 Free-Threaded 构建都会加载.abi3t.so扩展其他系统请咨询你的发行方并考虑更新这份指南。如果以wheel分发扩展使用以下标签Python 标签cp3{XX}其中XX是扩展所构建的最小 Python 版本例如设置了Py_TARGET_ABI3T为0x30f0000时就是cp315。ABI 标签abi3.abi3t。这是一个压缩标签集compressed tag set表示同时支持非 Free-Threaded 与 Free-Threaded 两种构建。例如wheel 文件名可能是myproject-1.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl如果文件名或标签不正确修正它们。测试注意当你构建兼容多个 CPython 版本的扩展时务必在每个支持的版本上例如 3.15、3.16 等等都进行测试。Stable ABI 只保证ABI兼容性行为也可能变化——既包括有意的变化由相关 PEP 覆盖也包括 bug。一定要在 Free-Threaded 与非 Free-Threaded 两种 CPython 构建上都跑测试。如果测试通过恭喜——你拥有了一个abi3t扩展。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考