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

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

2026-09-06 16:31:03作者:裴锟轩Denise

本篇基于 CPython 官方文档 Doc/howto/abi3t-migration.rst 编写,完整讲解自 3.15 版引入的 Free-Threaded Stable ABI(abi3t)迁移流程:如何设置构建目标宏、把 PyInit_ 初始化函数改写为 PEP 793 的 PyModExport_ 导出钩子、应对 PyObject 变不透明(opaque)后的自定义类型重定义与数据访问,以及最终的 wheel 标签与分发规范。读完并结合仓库源码佐证,你可以将一个已支持 abi3 的 C/C++ 扩展模块完整移植为一份同时兼容 Free-Threaded 与非 Free-Threaded 构建的 abi3t 扩展。

为什么迁移到 abi3t

使用 Stable ABI 的典型动机,是减少每个 CPython 版本需要构建和分发的产物数量。

不使用 Stable ABI 时,你必须在每个想支持的功能版本上分别构建一个共享库、以及一个对应的 wheel 分发包。下表每个标签都代表一个独立的库/wheel:

CPython 版本 非 Free-Threaded Free-Threaded
3.12 cpython-312
3.13 cpython-313 cpython-313t
3.14 cpython-314 cpython-314t
3.15 cpython-315 cpython-315t
3.16 cpython-316 cpython-316t
更后续版本 cpython-3{XX} cpython-3{XX}t

构建数量相当可观,再乘以支持的平台数就更可观了。

而 Stable ABI(abi3,CPython 3.2 引入)可以用每平台一个扩展覆盖所有非 Free-Threaded 构建:

CPython 版本 非 Free-Threaded Free-Threaded
3.12 abi3
3.13 (同一 abi3 cpython-313t
3.14 (同一 abi3 cpython-314t
3.15 (同一 abi3 cpython-315t
3.16 (同一 abi3 cpython-316t
更后续版本 (同一 abi3 cpython-3{XX}t

Free-Threaded 构建的 Stable ABI(abi3t),CPython 3.15 引入,对 Free-Threaded 构建做了同样的事——并且向下兼容非 Free-Threaded 构建:

CPython 版本 非 Free-Threaded Free-Threaded
3.12 abi3 *
3.13 (同一产物) cpython-313t
3.14 (同一产物) cpython-314t
3.15 及以后 abi3t(同时覆盖两种构建) (同一产物)

*(同上,abi3 扩展兼容所有非 Free-Threaded 构建;包括本表归给 abi3t 的 3.15+ 版本。)

什么时候不该这么做

Stable ABI 有两个主要缺点:

  1. 扩展可能变慢,因为 Stable ABI 优先考虑兼容性而非性能。差异通常不可察觉,且可以缓解:用同一份源码既构建 Stable ABI 版本,也针对"一级"CPython 版本构建少量版本专属版本。
  2. 并非所有 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 ABI(abi3)。若尚未支持,要么先完成那一步移植,要么按本指南操作但准备好修复文档未提及的问题。

Free-Threading 支持

虽然这不是硬性前置条件,但你大概率会在移植 abi3t 之前先把扩展适配 Free Threading。可参见仓库中的 Doc/howto/isolating-extensions.rst 等相关 how-to(原文档指向 freethreading-extensions-howto 一节)。

扩展模块的隔离

你的模块应当使用多阶段初始化(multi-phase initialization),并且要么已经完成隔离(isolated),要么保证每个进程最多加载一次。若不是这种情况,先按隔离扩展的指南操作(其中还有 opt-out 快捷方式一节)。

避免可变大小类型

如果你的扩展定义了可变大小的类型(使用 Py_tp_itemsizePyTypeObject.tp_itemsize),则无法移植到 3.15 的 abi3t

配置构建

如果使用构建工具(setuptools、meson-python、scikit-build-core 等),搜索其文档中如何选择 abi3t。文档撰写时尚不是所有工具都支持;如果你的工具支持,就直接用。你可以临时在 #include <Python.h> 之后加入以下代码来验证工具是否设置了正确的标志:

#if Py_TARGET_ABI3T+0 <= 0x30f0000
#error "abi3t define is not set!"
#endif

如果未设置,这里应当报出一个不同于 "abi3t define is not set" 的报错(即 Py_TARGET_ABI3T 未定义导致预处理失败)。

注意: 如果你的构建工具尚不支持 abi3t,在包含 Python.h 之前定义如下宏:

#define Py_TARGET_ABI3T 0x30f0000

或者作为编译器标志传入,例如 -DPy_TARGET_ABI3T=0x30f0000。一旦扩展在此设置下构建通过,它就与 CPython 3.15 及以后版本兼容。若手动设置该宏,之后还需要手动命名和打标签产物(见 标签与分发 一节)。

关于这个宏的底层语义,可以看仓库头文件 Include/pyabi.h

  • Py_TARGET_ABI3T 的合法值必须 >= 0x030f0000(3.15),否则直接 #error(pyabi.h 第 42–44 行);
  • 它也可以由 Py_LIMITED_APIPy_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_createPy_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_id + sl_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_FUNCInclude/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_DATAPySlot_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_GetDef
  • PyType_GetModuleByDef

检查你的代码是否使用了它们;如果没有,可跳过本节。

这些函数通常用于两个目的:

  1. 获取模块创建时使用的定义。 使用新 API 后这不再可能。模块不再持有对定义的引用,你需要想别的办法传递相关数据。

  2. 判断某个模块对象"是否是你的"。 这个用例现在由模块 token(module 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.h):

    void *token;
    if (PyModule_GetToken(module, &token) < 0) {
        /* 处理错误 */
    }
    

    PyType_GetModuleByDef 的调用:

    PyObject *module = PyType_GetModuleByDef(type, my_def);
    /* 处理错误;使用 module */
    

    换成 PyType_GetModuleByToken(返回强引用;声明见 Include/object.h):

    PyObject *module = PyType_GetModuleByToken(type, my_token);
    /* 处理错误;使用 module */
    Py_XDECREF(module);
    

PyObject 的不透明化(Opaqueness)

abi3t 中,PyObjectPyVarObject 结构变为不透明(opaque)——这正是 Include/pyabi.h_Py_OPAQUE_PYOBJECT 的作用。

访问它们的成员是被禁止的。如果你正在这样做,请改用其文档中提到的 getter/setter 函数来访问:

  • PyObject.ob_type
  • PyObject.ob_refcnt
  • PyVarObject.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、C++ auto 之类手段避免写类型名,这招就不灵了——要格外小心,并考虑运行未定义行为检测工具。)

然后,创建类时使用负数 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_GetBaseByToken

    PyTypeObject 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_HEXPY_MAJOR_VERSIONPY_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 poisonPy_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_ABI3T0x30f0000 时就是 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 扩展。

登录后查看全文
热门项目推荐
相关项目推荐