迁移到针对自由线程构建版的稳定 ABI (abi3t)

从 3.15 发布版起,CPython 支持 自由线程版 Python 专属的稳定 ABI:即 Stable ABI for Free-Threaded Builds,简写为 abi3t。 本文档描述了如何适配 C API 扩展以支持自由线程。

为什么要这样做

使用稳定版 ABI 的通常理由是减少你为你的库的每个版本进行构建并分发所需的专属文件数量。

如果没有稳定版 ABI,你必须为 CPython 的每个功能性版本构建一个单独的共享库,一个 wheel 发布包。 例如,下表的每个标签都代表一个单独的库/wheel:

CPython 版本

非自由线程

自由线程

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-3XX

cpython-3XXt

这需要许多次构建,特别是在乘以受支持的平台数量之后。

使用稳定 ABI (abi3,在 CPython 3.2 中引入),只要一个扩展库(每个平台)就可以覆盖 CPython 的所有 非自由线程版 构建:builds of:

CPython 版本

非自由线程

自由线程

3.12

abi3

---

3.13

cpython-313t

3.14

cpython-314t

3.15

cpython-315t

3.16

cpython-316t

之后的版本

cpython-3XXt

自由线程构建版稳定 ABI (abi3t),在 CPython 3.15 中引入,针对自由线程构建版完成同样的任务。 并且它还能兼容非自由线程版:

CPython 版本

非自由线程

自由线程

3.12

abi3 *

---

3.13

cpython-313t

3.14

cpython-314t

3.15

abi3t

3.16

之后的版本

*(如上所述,abi3 扩展与所有非自由线程构建版本兼容;即使是本表将其“归类”为 abi3t 的 3.15+ 版本也是如此。)

为什么*不*这样做?

稳定 ABI 主要有两个缺点。

首先,您的扩展程序运行速度可能会变慢,因为 Stable ABI 将兼容性置于性能之上。这种差异通常并不明显;而且,通常可以通过使用同一份源代码,同时构建 Stable ABI 版本以及针对“tier 1”版 CPython 的若干版本特定版本,来缓解这一问题。

其次,并非所有 C API 都可用。扩展需要被移植到 Stable ABI 的构建环境中,这一过程可能较为困难;在极少数情况下,甚至可能无法实现。

具体而言,abi3t 需要 CPython 3.15 中新增的 API。如果您希望使用同一源代码为较旧版本的 CPython 构建扩展,主要有两种选择:

  • 使用预处理条件语句。

    遵循本指南时,每当被要求执行会破坏您所关注的 CPython 版本构建的变更时,请使用 #ifdef Py_TARGET_ABI3T 代码块;将现有代码保留在 #else 代码块中。

    对于手写 C 扩展而言,由于 PEP 697 引入的增补内容,这种方法最低可适用于 CPython 3.12。对于代码生成器(例如 Cython)而言,保持与 3.11 及更早版本的兼容性可能仍然值得。

  • 请勿移植到 abi3t,继续为每个版本的 CPython 分别构建扩展,直到能够停止对旧版本的支持为止。

    这是一种可行的方法。并非所有扩展都需要立即切换到 abi3t。

前提条件

本指南假设您拥有一个直接用 C(或 C++)编写的扩展程序,并希望将其移植到 abi3t 中。

如果您的扩展使用了代码生成器(如 Cython)或语言绑定(如 PyO3),最好等到该工具开始支持 abi3t 后再进行;如果您维护此类工具,或许可以针对您的工具调整此处提供的说明。

非自由线程稳定 ABI

您的扩展应支持非自由线程版 Stable ABI(abi3)。如果不符合此要求,请先将其移植到相应版本;或者遵循本指南,但请做好修复指南未提及问题的准备。

自由线程支持

虽然从技术层面来说,这并不是一个硬性前提,但您很可能希望在将扩展移植到 abi3t 之前,先使其支持自由线程。请参阅 自由线程的 C API 扩展支持 中的说明。

参见

Porting Extension Modules to Support Free-Threading:面向扩展作者、由社区维护的移植指南。

分离扩展模块

您的模块应使用 multi-phase initialization;该模块应处于隔离状态,或将其限制为每个进程最多仅加载一次。如果不符合此情况,请先遵循 隔离扩展模块。(如需快捷方式,请参阅 opt-out section。)

避免使用可变大小类型

如果您的扩展定义了可变大小类型(使用 Py_tp_itemsize 或 PyTypeObject.tp_itemsize),则无法将其移植到 abi3t 3.15。

配置构建环境

如果您使用构建工具,请在其文档中搜索“abi3t”,并按照相关说明选择 ABI。作为参考,以下是几款常用构建工具的直接链接:

您可以尝试在 #include <Python.h> 之后临时添加以下代码,以验证该工具是否设置了正确的标志:

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

这应该会引发与“abi3t define 未设置”不同的错误。(如果你完全没有收到任何错误,那么要么是该模块已经完成了移植,要么是你没有编译所修改的代码。)

参见

Building and distributing abi3t extensions:在由社区维护的《Python 自由线程指南》中构建配置和轮子测试示例。

备注

如果您的构建工具尚未支持 abi3t,请在包含 Python.h 之前设置以下宏:

#define Py_TARGET_ABI3T 0x30f0000

或者将其指定为编译器标志,例如:

-DPy_TARGET_ABI3T=0x30f0000

一旦您的扩展基于此设置构建完成,它将兼容 CPython 3.15 及更高版本。

如果您手动设置此宏,之后还需要手动为生成的扩展命名并添加标签。相关内容将在下方的 标记与分发 中说明。

本指南会要求您进行一系列修改。每次修改后,请确认您的扩展程序仍能在原始(非 abi3t)配置下正常构建;理想情况下,还应针对您支持的所有 Python 版本进行测试。这样就能确保在移植过程中不会发生任何故障。

模块导出钩子

除非您已经完成这一步骤,否则您的扩展模块会定义一个名为 PyInit_<modname> 的 module initialization function (其中 modname 是您模块的名称)。您需要将其移植到 module export hook、PyModExport_<modname>;而 PEP 793 则是 CPython 3.15 中新增的一项功能。

您现有的 init 函数应如下所示(请将 <modname> 和 <moddef> 替换为您自己的名称):

PyMODINIT_FUNC
PyInit_<modname>(void)
{
    return PyModuleDef_Init(&<moddef>);
}

如果 return 之前存在某些代码,请将其移至 Py_mod_create 或 Py_mod_exec 插槽函数中。有关相关信息,请参阅 PyInit documentation。

该函数引用了一个 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 除外),并替换为你自己的值。

有关此 API 的详细信息,请参阅 PySlot 和 export hook 文档。

如示例所示,您的 PyModExport_ 函数应*仅*返回一个指向静态数据的指针。如果无法避免引入额外代码,请参阅 caveats in PyModExport documentation。

备注

使用 Setuptools 构建工具为 Windows 构建时,移除 PyInit_<modname> 函数可能会导致链接器错误 LINK : error LNK2001: unresolved external symbol PyInit_<modname>。这是由于 Setuptools 传递了 /EXPORT 链接器标志所致;由于 Python 3.15 起该标志已不再必要(参见 gh-141671),因此该操作是冗余的。一种临时解决方案是在代码中添加一个虚拟的 PyInit_<modname> 函数。如果存在 PyModExport_<modname>,Python 3.15+ 将永远不会调用此函数,因此该函数始终会失败:

// Workaround for https://github.com/pypa/distutils/issues/387
PyMODINIT_FUNC
PyInit_<modname>(void)
{
    PyErr_SetString(PyExc_SystemError,
                    "PyInit_* called for module with PyModExport_*");
    return NULL;
}

(此问题存在于 Setuptools 84.0.0 中;在更新版本中可能会得到修复。)

现有插槽

如果存在 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, create_module},
   {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;
   return 0;
}

static PyModuleDef_Slot my_slots[] = {
   ...
   /* (remove other Py_mod_exec slots) */
   ...
   {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, create_module),
    PySlot_FUNC(Py_mod_exec, my_module_exec),
    PySlot_END
};

如果执行此操作,请删除原始 PyModuleDef_Slot 数组及其 Py_mod_slots 条目。

关联 PyModuleDef

由于新 API 不采用 PyModuleDef 结构,因此不会为生成的模块关联任何定义。这将改变以下函数的行为:

请检查您的代码中是否包含这些内容。如果您不采用这些内容,可以跳过本节。

这些功能通常用于以下两种用途:

  1. 要获取模块创建时所采用的定义,使用新的 API 已无法实现这一点。模块不再保留对定义的引用,因此您需要另寻方法,以在程序间传递相关数据。

  1. 用于检查给定的模块对象是否“属于你”。目前,这一用例由 module tokens 提供——通过不透明指针来标识模块。若使用令牌,则需声明(或重用)一个唯一的静态变量,例如:

    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) {
        /* handle error */
    }
    

    以及来自 PyType_GetModuleByDef() 的调用,例如:

    PyObject *module = PyType_GetModuleByDef(type, my_def);
    /* handle error; use module */
    

    传递给 PyType_GetModuleByToken() (该操作会返回一个强引用):

    PyObject *module = PyType_GetModuleByToken(type, &my_token);
    /* handle error; use module */
    Py_XDECREF(module);
    

PyObject 不透明度

PyObject 和 PyVarObject 结构在 abi3t 中是不透明的。

禁止访问其成员。若需访问,请改用其文档中所述的 getter/setter 函数:

此外,PyObject 结构的 size 对编译器来说是未知的。 它在不同的 CPython 构建上可能 -- 而且 确实 -- 会发生变化。

备注

虽然该大小可在运行时获取(例如在 Python 代码中作为 sys.getsizeof(object())),但您应克制住从该值中计算指针偏移量的冲动。对象的内存布局可能会在未来的 abi3t 实现中发生变化。

自定义类型定义

由于 PyObject 是不透明的,传统的自定义类型定义方式已不再适用:

typedef struct {
   PyObject_HEAD  // expands to `PyObject ob_base;` which has unknown size

   int my_data;
} CustomObject;

static PyType_Spec CustomType_spec = {
   ...
   .basicsize = sizeof(CustomObject),
   ...
};

最有可能的情况是,您所有的类定义,以及 所有访问这些类数据的代码,都需要重新编写。这很可能就是您需要为支持 abi3t 而进行的最大规模改动。

对于每种此类类型,不要为整个实例定义一个 struct,而应仅为其定义一个仅包含“附加”字段的 struct——这些字段专属于该类,而非其超类:

typedef struct {
   int my_data;
} CustomObjectData;

更改名称。几乎所有使用该 struct 的代码都需要进行相应修改(尤其需要注意的是,指向新结构的指针无法从 PyObject* 或到 PyObject* 进行类型转换);更改名称后,相关用法会被标记为编译器错误。(如果您使用 typeof、C++ 的 auto 或类似方式来避免手动输入类型名称,则此方法将不起作用。请格外谨慎,并考虑运行相关工具来检测未定义行为。)

然后,要创建该类,请使用 negative basicsize 来表示“额外”存储空间,而不是 total 实例大小:

static PyType_Spec CustomType_spec = {
   ...
   .basicsize = -sizeof(CustomObjectData), /* note the minus sign */
   ...
};

如果您使用 Py_tp_members,请在每个成员上设置 Py_RELATIVE_OFFSET 标志,并针对您的新结构体指定 offset。

自定义类型数据访问

接下来是较为困难的部分:在所有需要访问此结构的代码中,您都需要额外调用一次 PyObject_GetTypeData(),以便从 PyObject * 中获取一个 CustomObjectData * 指针:

PyObject *obj = ...;
CustomObjectData *data = PyObject_GetTypeData(obj, cls);

请注意,此调用需要为您的类 (cls) 提供 type object。

如果您的类不支持子类化(即未使用 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 │
└──────────┴───┴────────────────┴───┴─────────────┴───┴─────────────┘

(省略号表示可能存在填充。请注意,这种内存布局并不具有确定性:Python 的未来版本可能会采用不同的填充方式,甚至改变各结构的排列顺序。)

获取正确的类主要有两种方式:

  • 在实例方法中,您的实现可以使用 PyCMethod 签名(以及 PyMethodDef.ml_flags 中的 METH_METHOD 位),并将类作为 defining_class 参数传入。

  • 否则,请使用 Py_tp_token 槽位为您的类分配一个唯一的静态标记,然后使用:

    PyTypeObject *cls;
    if (PyType_GetBaseByToken(Py_TYPE(obj), my_tp_token, &cls) < 0) {
        /* handle error */
    }
    CustomObjectData *data = PyObject_GetTypeData(obj, cls);
    Py_DECREF(cls);
    

    Type 令牌的工作方式与 earlier in this guide 中介绍的模块令牌类似。

避免在构建时使用条件语句

请检查您的代码中用于识别用于*构建*扩展时所用 Python 版本的 API。该版本现已不再与扩展运行时使用的 Python 版本保持一致,因此使用此信息的代码通常需要进行修改。需要检查的宏如下:

  • PY_VERSION_HEX, PY_MAJOR_VERSION, PY_MINOR_VERSION:

    • 如需获取运行时版本,请使用 Py_Version;

    • 要确定可用的 C API,请使用 Py_TARGET_ABI3T。该宏被设置为最低支持的版本。

  • Py_GIL_DISABLED:在 abi3t 之下,此宏始终会被定义。在自由线程化 Python 中运行的代码*应当*也能在启用 GIL 的情况下正常运行(因为 GIL 可以在运行时启用),而且通常*确实*可以正常运行(除非出于某种原因,该代码需要同时使用多个 attached thread state)。

进一步的代码修改

如果仍出现编译器错误或警告,请设法予以修复。遗憾的是,本指南内容有限,无法涵盖扩展可能需要的所有代码变更。

如果您发现其他扩展作者可能会遇到的问题,请考虑为本指南提交 reporting an issue (或发送 pull request)。

目前版本的 abi3t 可能无法修复您的问题。若如此,提交报告或许有助于该问题在 CPython 的下一个版本中被列为优先处理事项。

标记与分发

如果您使用的构建工具支持 abi3t,那么您的扩展已准备就绪;但您可能仍需要确认其构建是否正确。

使用 abi3t 构建的扩展应包含以下扩展:

  • 在 Windows 上:.pyd (与其他扩展名一样);

  • Linux、macOS 以及其他使用 .so 后缀的系统:.abi3t.so (不是 .cpython-315t.so 或 .abi3.so)。请注意,无论是自由线程版还是非自由线程版构建版本,都会加载 .abi3t.so 扩展;

  • 其他系统:请咨询您的经销商,或许还可以更新本指南。

如果你要以 wheel 的形式分发扩展库,则使用下列标签:

  • Python 标签: cp3XX,其中 XX 是扩展构建对应的最低 Python 版本。 (例如,如果你将 Py_TARGET_ABI3T 设为 0x30f0000 则为 cp315。 请参阅 针对稳定 ABI 编译 了解更多可用的值。)

  • ABI 标签: abi3.abi3t。 这是一个表示同时支持非自由线程和自由线程构建的 压缩标签集。

例如,wheel 文件名可以是这样的:

myproject-1.0-cp315-abi3.abi3t-macosx_11_0_arm64.whl

参见

在 PyPA 包分发元数据中的 平台兼容性标签。

如果文件名或标签不正确,请修正它们。

测试

请注意当你要构建兼容多个 CPython 版本的扩展时,你应当总是用它所支持的每个版本来 测试 它(例如 3.15, 3.16 等等)。 稳定版 ABI 仅会保证 ABI 兼容性;此外还可能存在行为变更 -- 包括有意的(已由 PEP 387 覆盖)和程序缺陷。

请确保同时在 CPython 的自由线程和非自由线程构建上运行测试。

如果它们都通过,祝贺你! 你将得到一个 abi3t 扩展。