PEP 809 – 面向未来的稳定ABI
摘要
现有的稳定ABI(abi3)已无法继续维护,需要被替代。abi2026将成为首个替代方案,解决当前已知的不兼容问题,并计划至少支持10年后退役。下一个ABI(例如abi2031)将与前一版本至少有5年的重叠期。
长期稳定性将通过运行时ABI发现机制实现,允许扩展在支持相同ABI的早期版本上运行。在ABI生命周期内的变更和新增功能可以作为接口添加,使其能够在运行时被发现,这样调用者可以选择合适的回退行为。
abi3 ABI将在启用GIL的构建中保留至少五年,之后可能会退役(仅提供abi2026及更高版本)。自由线程构建不支持abi3,因此它们的第一个稳定ABI将是abi2026。
术语
本PEP使用“GIL启用构建”作为“自由线程构建”的反义词,即未使用Py_GIL_DISABLED构建的解释器或扩展。
动机
稳定ABI目前不适用于自由线程构建。当定义Py_LIMITED_API时,扩展将无法构建。同样,为CPython的GIL启用构建构建的扩展在自由线程构建上无法加载(或崩溃)。
指导委员会在PEP 779的接受声明中表示,“期望为Python 3.15准备和定义自由线程的稳定ABI”。本PEP提出了一个与3.15及以后所有变体兼容的稳定ABI,允许包开发者生成单个扩展构建。
相关PEP
PEP 803是本提案的替代方案,本PEP中使用的许多背景文本故意与其相同。我们感谢PEP 803作者使用他们的文本。
背景
Python的稳定ABI,如PEP 384和PEP 652中所定义,提供了一种编译扩展模块的方法,这些模块可以加载到CPython解释器的多个次要版本上。
一些项目使用此功能来限制需要为每个版本构建和分发的wheel(二进制工件)数量,和/或使其更容易使用Python的预发布版本进行测试。
随着自由线程构建(PEP 703)有望最终成为默认(PEP 779),我们需要一种方法使稳定ABI可用于这些构建。
要针对稳定ABI构建,扩展必须使用有限API,即仅使用CPython公开的函数、结构等的子集。有限API是版本化的,针对有限API 3.X构建会产生与CPython 3.X及任何更高版本ABI兼容的扩展(尽管CPython中的错误有时会导致实际不兼容)。
此外,有限API不是“稳定的”:较新版本可能会删除旧版本中可用的API项。本PEP提出了对有限API和稳定ABI版本化的重大更改。目标是实现稳定性和兼容性的长期管理,同时允许有限子集用户访问后续Python版本中的创新。
设计原理
本PEP中的设计基于以下几个假设:
- 单一ABI:单个编译的扩展模块应支持自由线程和GIL启用构建。
- 无向后兼容性:新的有限API将不被CPython 3.14及以下版本支持。需要此支持的项目可以专门为3.14自由线程解释器和旧的稳定ABI版本构建单独的扩展。
- API更改可以接受:新的有限API可能要求扩展作者对其代码进行重大更改。无法执行此操作的项目可以继续使用有限API 3.14,这将产生仅与GIL启用构建兼容的扩展。
- 无额外配置:我们不引入新的“旋钮”来影响哪些API可用以及ABI与什么兼容。
规范
请注意,大部分规范与PEP 803相同,读者应参考该提案以获取详细信息。ABI稳定性、构建时宏和接口API部分是本提案独有的。
ABI稳定性
稳定ABI将冻结至少10年。当新版本的稳定ABI被冻结时,现有版本将继续支持至少5年。这为包维护者(和其他用户)提供了充足的迁移时间,以同时迁移其支持的整个发布范围。但是,如果Python核心开发团队认为没有理由替换当前的稳定ABI,可能会推迟冻结新版本。
新稳定ABI使用PEP流程定义,其名称反映支持它的第一个运行时的发布年份。当稳定ABI被冻结时,年份成为ABI的名称。例如,我们预计此方案下的第一个ABI将是abi2026,并将被所有版本支持直到至少2036年。如果在2036年停止支持,则abi2031将成为迁移目标,允许包开发者在自己的迁移后支持至少五年的发布。
冻结时,不允许任何ABI更改。不允许添加,也不允许删除、修改或剧烈的语义更改。关键的是,针对特定ABI编译的扩展模块必须针对支持该ABI的任何Python版本(早期或后期)成功加载(即所有导入的符号在所有支持的平台上都满足)。
不允许无法通过现有兼容ABI在运行时检测到的语义更改。也就是说,检测当前Python版本上是否预期特定行为的API必须已在支持该ABI的所有早期版本上可用。
不透明PyObject
有限API的3.15版本将使许多结构不透明,这样用户无法对其大小或布局做出任何假设。详细信息可在PEP 803中找到,此处的提案是相同的。
新导出钩子(PEP 793)
实现此PEP需要接受PEP 793(PyModExport:C扩展模块的新入口点),为定义扩展模块提供新的“导出钩子”。使用新钩子将在有限API 3.15中成为强制要求。此提案与PEP 803相同。
运行时ABI检查
有关详细信息,请参阅PEP 803。此提案是相同的。
构建时宏
我们要求将Py_LIMITED_API定义为0x03ff_YYYY - 即,高字是常量0x03ff,而低字是ABI名称(年份)作为十六进制值。虽然这导致十进制值与年份不同,但我们认为这不重要,因为该值是任意标签,更可能指定为常量(在cc命令行中)而不是计算值。
使用0x03ff作为常量旨在允许与早期运行时的兼容性。当与仅支持abi3的标头一起使用时,相同的常量将选择该版本中可用的“最完整”ABI3版本。例如,在3.15+中使用0x03ff2026将选择abi2026,而在3.10中将选择适用于3.10-3.14的ABI3版本。
Wheel标签
Wheel应标记为ABI标签abi2026。不需要更改Python或平台标签。或许值得注意的是,标记为cp314或更早版本的发布永远不会与abi2026兼容,因为它不存在,因此标记为py3-abi2026-
新API
实现此PEP将使得可以构建可以在自由线程Python上成功加载的扩展,但不一定是没有GIL的线程安全扩展。允许没有GIL的线程安全性的有限API – 大概是PyMutex、PyCriticalSection和类似物 – 将通过C API工作组或在后续PEP中添加。
接口API
将向Python和新的有限API添加新的接口API。此API是为了满足上面ABI稳定性部分中的“在所有发布上可检测语义更改”的要求。也就是说,消费者[1]将能够立即采用新API,使用最新发布为有限API编译,并保留支持该ABI的所有发布的二进制兼容性。
简而言之,主要API是PyObject_GetInterface(),它委托给新的仅本地类型槽来填充包含数据或函数指针的C结构。因为C结构定义嵌入到扩展中,而不是在运行时获得,扩展模块可以在针对不提供它的Python发布运行时意识到后续结构。
如果对PyObject_GetInterface的调用请求当前版本上不可用的结构,或为提供的对象不可用,则调用安全失败。调用者然后可以使用回退逻辑(例如,使用抽象Python API)或中止,基于他们的偏好。
例如,如果在abi2026的生命周期内添加了一个新API,允许更有效地访问int对象的内部数据,而不是添加新API,我们将创建一个新接口:一个包含函数指针以将数据复制到新位置的结构,以及该接口的先前未使用的索引/名称。调用者可以首先调用PyObject_GetInterface(int_object, &intf_struct);如果成功,调用(假设的)(*intf_struct.copy_bits)(&intf_struct, dest, sizeof(dest));如果失败,他们可以使用PyObject_CallMethod(int_object, "to_bytes", ...)执行相同的操作,但效率较低。此示例的最终结果是单个扩展模块,它与支持abi2026的所有发布二进制兼容,但在针对Python的较新发布运行时更高效。
概述完成,以下是每个新API的完整规范:
// 请求对象(或类型)接口的抽象API。
PyAPI_FUNC(int) PyObject_GetInterface(PyObject *obj, void *intf);// 释放接口的API。
PyAPI_FUNC(int) PyInterface_Release(void *intf);// 每个接口开始的预期布局。实际接口结构将添加额外的函数指针或数据。
typedef struct PyInterface_Base {// sizeof(self),用于额外验证调用者传递正确的结构。Py_ssize_t size;// 结构的唯一标识符。详细信息如下。uint64_t name;// 释放结构的函数(例如,减少任何PyObject字段的引用计数)。// 应仅由PyInterface_Release()调用,不直接调用。int (*release)(struct PyInterface_Base *intf);
} PyInterface_Base;// PyTypeObject字段的类型槽定义。
typedef int (*Py_getinterfacefunc)(PyObject *o, PyInterface_Base *intf);
结构的唯一标识符是定义为宏的64位整数(以确保编译的扩展模块嵌入值,而不是尝试在运行时发现它)。高32位是命名空间,定义自己结构的实现者应为自己选择唯一值。零保留给CPython。
接口名称用于标识结构布局,因此任何定义的对象可以重用来自另一个命名空间的接口名称,前提是结构匹配。这是有意的,因为它允许第三方类型实现与核心类型相同的接口,而不必依赖共享实现。明确地说,为CPython定义的接口可以被其他扩展模块使用,而无需更改名称或名称的命名空间。
例如,考虑一个实现PyDict_GetItemString()的假设接口。核心dict类型可能进行内部优化以按字符串键定位条目,而外部类型可以使用相同的接口进行自己的优化。对调用者来说,它似乎使用相同的接口,因此调用者与更广泛的类型兼容,而不是如果使用(例如)CPython的具体对象API。
接口名称不能在任何时候从标头中删除,结构定义只能在支持它们的所有稳定ABI版本完全退役时删除。但是,即使早期发布返回了它们,对象可能停止返回特定接口,如果它不再被推荐或可靠。如果适当,可以使用运行时弃用警告,未指定特定规则。
接口结构是固定的,不能更改。当需要更改时,应使用新名称定义新接口。为接口结构添加的字段是公共API,应记录。不打算直接使用的字段应以下划线开头,否则不能设为“私有”。接口可以提供数据和函数指针的混合,或使用强PyObject *引用以避免竞争条件。
检索接口后,接口必须保持有效直到被释放,即使对象的引用被释放。接口的行为可以以适当的方式处理底层对象的更改,但可能应记录其选择。拥有两个类似接口以不同方式处理这些更改并非不合理(例如,一个接口在接口的生存期内锁定对象,而另一个不锁定)。
添加新有限API的过程有所改变:不是拥有随着每个发布而增长的ABI,新API可以作为真实函数添加(当未使用有限API时),但应作为静态内联函数为有限API添加。此静态内联函数应使用接口在运行时检测功能,并包括抽象回退或适当的异常。
这意味着消费者可以立即采用新API,使用最新发布为有限API编译,并保留支持相同稳定ABI的所有发布的二进制兼容性。在下一次稳定ABI冻结时,API可以提升为新的稳定ABI/有限API作为真实函数,或保留为接口。
向后兼容性
有限API 3.15将不向后兼容旧的CPython发布,因为删除了结构和函数。无法切换的扩展作者可以继续使用有限API 3.14及以下以在GIL启用构建上使用。
不会对GIL启用构建的abi3进行更改,所有现有符号将保持可用,即使这些在新稳定ABI下不再可用。
使自由线程构建成为CPython的默认/唯一发布将是向后不兼容的更改,扩展作者将需要迁移。
安全影响
未知。
如何教授此内容
Python的本机ABI可以描述为定期更新的标准或规范,按年份标识,类似于其他语言。任何扩展模块都可以使用此ABI,并在其分发信息中声明它们期望的ABI。任何Python实现可以选择支持特定的ABI版本,任何也支持该版本的扩展应该是可用的。
从abi3迁移到新ABI可能涉及源代码更改,但可以视为一次性任务。在许多(如果不是大多数)情况下,源代码将与abi3和新ABI兼容,简化旧发布和当前发布的构建生产。通常,abi3构建应使用最早支持的CPython运行时构建,而新ABI构建应使用最新的CPython运行时(或另一个兼容的运行时)构建。
从一个ABI(例如abi2026)迁移到下一个(例如abi2031)应是手动任务。ABI更新之间有足够的重叠,大多数项目一次只需要支持一个,如果它们自己的支持矩阵允许,可以一次更新所有构建。不期望包维护者立即支持每个新ABI。
前向和后向兼容性通过动态接口检测确保。使用最近添加的有限API函数的代码将在旧发布上运行,尽管可能性能较低。请参阅新函数的文档以查找任何有限API特定细微差别。
非C调用者应直接使用接口机制以访问新功能,而不会人为地将它们的兼容性限制到较新发布。接口的名称和结构布局保证永远稳定,尽管不应假设接口将永远可用,并且应包括适当的回退代码(替代实现或错误处理)。
参考实现
有关从该PEP继承的方面的参考实现链接,请参阅PEP 803。
接口的参考实现是zooba/cpython#44。
拒绝的想法
[暂时参见讨论。]
未解决的问题
[暂时参见讨论。]
脚注
[1]
我们使用“消费者”一词包括任何针对(“消费”)C API编码的人。这主要是本地扩展模块的开发人员(有时是“包开发人员”),但也包括托管CPython的应用程序开发人员以及在运行时与CPython接口交互的人(如调试器或跨运行时代理工具)。
版权
本文件置于公共领域或CC0-1.0-Universal许可证下,以更宽松者为准。
更多精彩内容 请关注我的个人公众号 公众号(办公AI智能小助手)或者 我的个人博客 https://blog.qife122.com/
对网络安全、黑客技术感兴趣的朋友可以关注我的安全公众号(网络安全技术点滴分享)
公众号二维码

公众号二维码
