PEP 733 – Python 公共 C API 评估
- 作者:
- Erlend Egeberg Aasland <erlend at python.org>, Domenico Andreoli <domenico.andreoli at linux.com>, Stefan Behnel <stefan_ml at behnel.de>, Carl Friedrich Bolz-Tereick <cfbolz at gmx.de>, Simon Cross <hodgestar at gmail.com>, Steve Dower <steve.dower at python.org>, Tim Felgentreff <tim.felgentreff at oracle.com>, David Hewitt <1939362+davidhewitt at users.noreply.github.com>, Shantanu Jain <hauntsaninja at gmail.com>, Wenzel Jakob <wenzel.jakob at epfl.ch>, Irit Katriel <irit at python.org>, Marc-Andre Lemburg <mal at lemburg.com>, Donghee Na <donghee.na at python.org>, Karl Nelson <nelson85 at llnl.gov>, Ronald Oussoren <ronaldoussoren at mac.com>, Antoine Pitrou <solipsis at pitrou.net>, Neil Schemenauer <nas at arctrix.com>, Mark Shannon <mark at hotpy.org>, Stepan Sindelar <stepan.sindelar at oracle.com>, Gregory P. Smith <greg at krypto.org>, Eric Snow <ericsnowcurrently at gmail.com>, Victor Stinner <vstinner at python.org>, Guido van Rossum <guido at python.org>, Petr Viktorin <encukou at gmail.com>, Carol Willing <willingc at gmail.com>, William Woodruff <william at yossarian.net>, David Woods <dw-git at d-woods.co.uk>, Jelle Zijlstra <jelle.zijlstra at gmail.com>
- 状态:
- 最终版
- 类型:
- 信息性
- 创建日期:
- 2023年10月16日
- 发布历史:
- 2023年11月1日
摘要
这份信息性 PEP 描述了我们对公共 C API 的共同看法。该文档定义了
- C API 的目的
- 利益相关者及其特定用例和要求
- C API 的优势
- C API 的问题,分为九个弱点领域
本文件不提出任何已识别问题的解决方案。通过创建 C API 问题共享列表,本文件将有助于指导有关变更提案的持续讨论并确定评估标准。
引言
Python 的 C API 最初并非为当前所服务的所有不同目的而设计。它从最初作为解释器 C 代码与 Python 语言和库之间的内部 API 演变而来。在其最初版本中,它被暴露出来,使得可以将 Python 嵌入到 C/C++ 应用程序中,并用 C/C++ 编写扩展模块。这些功能对于 Python 生态系统的发展至关重要。几十年来,C API 不断发展,提供了不同级别的稳定性,约定也发生了变化,并出现了新的使用模式,例如与其他语言(而非 C/C++)的绑定。未来几年,预计会有新的发展进一步考验 C API,例如 GIL 的移除和 JIT 编译器的开发。然而,这种发展并未得到明确文档化的指导方针支持,导致 CPython 不同子系统中的 API 设计方法不一致。此外,CPython 不再是唯一的 Python 实现,它在设计时所做的一些决策,对于其他实现来说难以处理 [问题 64]。与此同时,C API 的设计和实现都吸取了教训并识别了错误。
由于向后兼容性约束及其固有的技术和社会复杂性,演进 C API 变得困难。不同类型的用户带来了不同(有时相互冲突)的需求。在提出增量改进建议时,稳定性与进步之间的权衡是一个持续的、备受争议的讨论话题。已经提出了几个改进、重新设计或替换 C API 的提案,每个都代表了对问题的深入分析。在 2023 年语言峰会上,连续三场会议专门讨论了 C API 的不同方面。普遍的共识是,新的设计可以弥补 C API 在过去 30 年中积累的问题,同时更新它以适应最初并非为其设计的用例。
然而,在语言峰会上,也有人认为我们正在尝试讨论解决方案,但对我们正在努力解决的问题缺乏清晰的共同理解。我们决定,在能够评估任何提出的解决方案之前,我们需要就 C API 的当前问题达成一致。因此,我们在 GitHub 上创建了 capi-workgroup 仓库,以收集每个人对该问题的想法。
在该仓库中创建了 60 多个不同的问题,每个问题都描述了 C API 的一个问题。我们对它们进行了分类,并确定了一些重复出现的主题。下面的部分主要对应于这些主题,每一部分都包含对该类别中提出的问题的综合描述,以及指向各个问题的链接。此外,我们还包含了一个旨在识别 C API 不同利益相关者及其各自要求的章节。
C API 利益相关者
正如引言中所述,C API 最初是作为 CPython 解释器和 Python 层之间的内部接口创建的。后来它被公开,作为第三方开发者扩展和嵌入 Python 程序的一种方式。多年来,随着新类型的利益相关者的出现,他们拥有不同的需求和关注领域。本节从不同利益相关者需要通过 C API 执行的操作方面描述了这种复杂的状况。
所有利益相关者的共同行动
有一些通用操作,所有类型的 API 用户都需要
- 定义函数并调用它们
- 定义新类型
- 创建内置类型和用户定义类型的实例
- 对对象实例执行操作
- 内省对象,包括类型、实例和函数
- 引发和处理异常
- 导入模块
- 访问 Python 的操作系统接口
以下章节将探讨各个利益相关者的独特需求。
扩展模块编写者
扩展模块编写者是 C API 的传统用户。他们的要求是上面列出的常见操作。他们通常还需要
- 创建新模块
- 在 C 级别高效地进行模块间接口
Python 实现
Python 实现,如 CPython、PyPy、GraalPy、IronPython、RustPython、MicroPython 和 Jython),可能对不同子系统的实现采取非常不同的方法。它们需要
- API 是抽象的,并隐藏实现细节。
- API 的规范,最好附带一个确保兼容性的测试套件。
- 最好有一个可以在 Python 实现之间共享的 ABI。
替代 API 和绑定生成器
有几个项目实现了 C API 的替代方案,这些方案为扩展模块用户提供了直接使用 C API 编程所不具备的优势。这些 API 是通过 C API 实现的,在某些情况下,通过使用 CPython 内部机制来实现。
还有一些库在 Python 和其他对象模型、范例或语言之间创建绑定。
这些类别之间存在重叠:绑定生成器通常提供替代 API,反之亦然。
例如,C++ 的 Cython、cffi、pybind11 和 nanobind;Rust 的 PyO3;PySide 用于 Qt 的 Shiboken;GTK 的 PyGObject;Go 的 Pygolo;Java 的 JPype;Android 的 PyJNIus;Objective-C 的 PyObjC;C/C++ 的 SWIG;.NET (C#) 的 Python.NET;HPy、Mypyc、Pythran 和 pythoncapi-compat。CPython 用于解析函数参数的 DSL,即 Argument Clinic,也可以被视为属于这一类利益相关者。
替代 API 需要最小化的构建块来高效访问 CPython。它们不一定需要符合人体工程学的 API,因为它们通常生成不旨在供人类阅读的代码。但它们确实需要足够全面,以便它们可以避免访问内部机制,而不会牺牲性能。
绑定生成器通常需要
- 创建自定义对象(例如函数/模块对象和回溯条目),使其行为与等效 Python 代码尽可能接近。
- 动态创建传统 C 扩展中静态的对象(例如类/模块),并需要 CPython 来管理它们的状态和生命周期。
- 以低开销动态适配外部对象(字符串、GC 管理的容器)。
- 将外部机制、执行模型和保证适配到 Python 方式(栈式协程、续体、单写多读语义、虚拟多重继承、1 基索引、超长继承链、goroutine、通道等)。
这些工具也可能受益于更稳定和更快(可能更底层)的 API 之间的选择。它们的用户可以决定是否能够经常重新生成代码,或者为了更高的稳定性和更少的维护工作而牺牲一些性能。
C API 的优势
尽管本文大部分篇幅致力于 C API 的问题,我们希望在任何新设计中解决这些问题,但指出 C API 的优势并确保它们得以保留也同样重要。
正如引言中所述,C API 使得 Python 生态系统在过去三十年中得以发展和壮大,同时不断演进以支持其最初并未设计的用例。这一记录本身就表明了其有效性和价值。
在 capi-workgroup 的讨论中提到了许多具体的优点。堆类型被认为比静态类型更安全、更易于使用 [问题 4]。
基于 Python 字符串查找的 C API 函数接受 C 字符串字面量非常方便 [问题 30]。
有限 API 表明,隐藏实现细节的 API 更易于 Python 的演进 [问题 30]。
C API 的问题
本文档的其余部分总结并分类了在 capi-workgroup 仓库中报告的问题。这些问题被分为几个类别。
API 演进和维护
C API 变更的难度是本报告的核心。它隐含在我们在此讨论的许多问题中,特别是当我们决定增量错误修复是否可以解决问题,或者只能作为 API 重新设计的一部分来解决时 [问题 44]。每次增量变更的好处通常被认为太小,不足以证明其带来的干扰。随着时间的推移,这意味着我们在 API 设计或实现中犯的每一个错误都将无限期地伴随着我们。
我们可以对这个问题有两种看法。一种是,这是一个问题,解决方案需要融入我们设计的任何新的 C API 中,以增量式 API 演进过程的形式,其中包括 API 元素的弃用和移除。另一种可能的做法是,这不是一个需要解决的问题,而是任何 API 的一个特性。在这种观点下,API 演进不应该是增量的,而应该是通过大型重新设计,每次重新设计都吸取过去的错误教训,并且不受向后兼容性要求的束缚(与此同时,可以添加新的 API 元素,但永远不能删除任何东西)。一种折衷方法介于这两个极端之间,修复那些容易或足够重要以进行增量解决的问题,而将其他问题保留下来。
我们在 CPython 中面临的问题是,我们没有就 API 演进达成一致的官方方法。核心团队的不同成员朝着不同的方向努力,这是持续分歧的根源。任何新的 C API 都需要就其维护模型,以及实现该模型的技??和组织流程,做出明确的决定。
如果该模型确实包含 API 增量演进的规定,它将包括管理变更对用户影响的流程 [问题 60],或许通过引入外部向后兼容模块 [问题 62],或新的“受祝福”函数 API 层级 [问题 55]。
API 规范和抽象
C API 没有正式规范,目前它被定义为参考实现(CPython)在特定版本中包含的任何内容。文档作为不完整的描述,不足以验证完整 API、有限 API 或稳定 ABI 的正确性。因此,C API 可能会在版本之间发生显著变化,而无需更显眼的规范更新,这导致了许多问题。
非 C/C++ 语言的绑定必须解析 C 代码 [问题 7]。某些 C 语言特性难以通过这种方式处理,因为它们会产生编译器依赖的输出(例如枚举)或需要 C 预处理器/编译器而不仅仅是解析器(例如宏) [问题 35]。
此外,C 头文件往往会暴露超出公共 API 预期的内容 [问题 34]。特别是,可能会暴露实现细节,例如内部数据结构的精确内存布局 [问题 22 和 PEP 620]。这可能会使 API 演变变得非常困难,特别是在稳定 ABI 中发生这种情况时,例如 ob_refcnt 和 ob_type 通过引用计数宏访问的情况 [问题 45]。
我们发现了与引用计数暴露方式相关的更深层次问题。C 扩展模块需要通过调用 Py_INCREF 和 Py_DECREF 来管理引用,这种方式是 CPython 内存模型特有的,对于其他 Python 实现来说很难模拟。 [问题 12]。
另一组问题源于 PyObject* 在 C API 中被暴露为实际指针而非句柄的事实。对象的地址用作其 ID 并用于比较,这使得在 GC 期间移动对象的其他 Python 实现复杂化 [问题 37]。
一个独立的问题是,对象引用对于运行时来说是不透明的,只能通过调用 tp_traverse/tp_clear 来发现,这些函数有自己的目的。如果有一种方法让运行时了解对象图的结构,并跟上它的变化,这将使替代实现能够实现不同的内存管理方案 [问题 33]。
对象引用管理
对于函数,没有一致的命名约定能够明确其引用语义,这导致了易出错的 C API 函数,它们不遵循典型的行为。当 C API 函数返回 PyObject* 时,调用者通常获得对该对象的引用所有权。然而,也有例外情况,函数返回“借用”引用,调用者可以访问但并不拥有其引用。类似地,函数通常不会改变其参数引用的所有权,但也有例外情况,函数“窃取”引用,即通过调用将引用的所有权从调用者永久转移给被调用者 [问题 8 和 问题 52]。用于描述这些情况的文档术语也可以改进 [问题 11]。
对于返回“借用”引用(例如 PyList_GetItem)的函数 [问题 5 和 问题 21],或指向对象内部结构部分的指针(例如 PyBytes_AsString)的函数 [问题 57],需要更彻底的改变。在这两种情况下,引用/指针在拥有对象持有引用期间有效,但这个时间很难推断。此类函数不应在 API 中存在,除非有机制可以使其安全。
对于容器,API 目前缺少对所包含对象引用进行批量操作的功能。这对于稳定 ABI 尤其重要,因为 INCREF 和 DECREF 不能是宏,当作为一系列函数调用实现时,批量操作会变得昂贵 [问题 15]。
类型定义和对象创建
C API 包含一些函数,可能创建不完整或不一致的 Python 对象,例如 PyTuple_New 和 PyUnicode_New。当对象被 GC 跟踪或其 tp_traverse/tp_clear 函数被调用时,这会导致问题。一个相关的问题是 PyTuple_SetItem 等函数,它用于修改部分初始化的元组(元组一旦完全初始化后是不可变的) [问题 56]。
我们发现了一些类型定义 API 的问题。由于历史原因,tp_new 和 tp_vectorcall 之间经常存在大量的代码重复 [问题 24]。类型槽函数应该间接调用,以便其签名可以改变以包含上下文信息 [问题 13]。类型定义和创建过程的几个方面定义不明确,例如该过程的哪个阶段负责初始化和清除类型对象的不同字段 [问题 49]。
错误处理
C API 中的错误处理基于存储在线程状态(全局范围)中的错误指示器。设计意图是每个 API 函数返回一个值,指示是否发生了错误(按照惯例,-1 或 NULL)。当程序知道发生了错误时,它可以获取存储在错误指示器中的异常对象。我们发现了一些与错误处理相关的问题,这些问题指向了过于容易被错误使用的 API。
有些函数在执行过程中不会报告所有发生的错误。例如,PyDict_GetItem 在调用键的哈希函数或在字典中执行查找时,会清除所有发生的错误 [问题 51]。
Python 代码在执行时永远不会出现未处理的异常(根据定义),通常使用原生函数的代码也应该被引发的错误中断。这在大多数 C API 函数中没有检查,并且在解释器中,一些错误处理代码在异常已设置时调用 C API 函数。例如,请参见 _PyErr_WriteUnraisableMsg 的错误处理程序中对 PyUnicode_FromString 的调用 [问题 2]。
有些函数不返回任何值,因此调用者被迫查询错误指示器以识别是否发生了错误。一个例子是 PyBuffer_Release [问题 20]。还有其他函数有返回值,但该返回值不能明确指示是否发生了错误。例如,PyLong_AsLong 在发生错误时返回 -1,或者当参数的值确实是 -1 时 [问题 1]。在这两种情况下,API 都容易出错,因为错误指示器可能在函数调用之前就已经设置,并且错误被错误归因。未在调用之前检测到错误是调用代码中的一个 bug,但程序在这种情况下的行为使得识别和调试问题变得困难。
有些函数接受一个 PyObject* 参数,当其为 NULL 时具有特殊含义。例如,如果 PyObject_SetAttr 接收 NULL 作为要设置的值,则表示应该清除该属性。这容易出错,因为 NULL 可能表示值构建中的错误,而程序未能检查此错误。程序会将 NULL 错误地解释为非错误含义 [问题 47]。
API 层级和稳定性保证
不同的 API 层级提供了稳定性与 API 演进之间的不同权衡,有时还包括性能。
稳定 ABI 被确定为需要深入研究的领域。目前它不完整且未被广泛采用。与此同时,它的存在使得对某些实现细节进行更改变得困难,因为它暴露了结构字段,例如 ob_refcnt、ob_type 和 ob_size。关于是否值得保留稳定 ABI 存在一些讨论。双方的论点可以在 [问题 4] 和 [问题 9] 中找到。
或者,有人建议为了能够演进稳定 ABI,我们需要一种机制来支持在同一个 Python 二进制文件中同时存在多个版本的 ABI。有人指出,在单个 ABI 版本中对单个函数进行版本控制是不够的,因为可能需要同时演进一组相互操作的函数 [问题 39]。
有限 API 于 3.2 版本引入,作为 C API 的一个受认可子集,建议希望将自己限制在高质量且不易频繁更改的 API 的用户使用。Py_LIMITED_API 标志允许用户将其程序限制为旧版本的有限 API,但我们现在需要相反的选项,即排除旧版本。这将使通过替换有缺陷的元素来演进有限 API 成为可能 [问题 54]。更一般地,在重新设计中,我们应该重新审视 API 层级的指定方式,并考虑设计一种统一我们目前在不同层级之间进行选择的方法 [问题 59]。
名称以下划线开头的 API 元素被视为私有,本质上是一个没有稳定性保证的 API 层级。然而,这只是最近在 PEP 689 中才明确的。对于早于 PEP 689 的此类 API 元素,变更策略应该是什么尚不清楚 [问题 58]。
有一些 API 函数既有不安全(但快速)的版本,也有执行错误检查的安全版本(例如,PyTuple_GET_ITEM 与 PyTuple_GetItem)。将它们分组到各自的层级中可能会有所帮助——“不安全 API”层级和“安全 API”层级 [问题 61]。
C 语言的使用
关于 CPython 使用 C 语言的方式,提出了一些问题。首先是我们使用的 C 方言以及如何测试其兼容性,以及 API 头文件与 C++ 方言的兼容性 [问题 42]。
API 中 const 的使用目前很稀疏,但不清楚这是否是我们需要考虑改变的地方 [问题 38]。
我们目前使用 C 类型 long 和 int,而固定宽度整数,如 int32_t 和 int64_t,现在可能是更好的选择 [问题 27]。
我们正在使用其他语言难以交互的 C 语言特性,例如宏、可变参数、枚举、位域和非函数符号 [问题 35]。
API 中有些函数接受 PyObject* 参数,但该参数必须是更特定的类型(例如 PyTuple_Size,如果其参数不是 PyTupleObject* 则会失败)。这是一个悬而未决的问题,即这是否是一个好的模式,或者 API 是否应该期望更特定的类型 [问题 31]。
API 中有些函数接受具体类型,例如 PyDict_GetItemString,它对以 C 字符串而非 PyObject* 指定的键执行字典查找。与此同时,对于 PyDict_ContainsString,不认为添加具体类型替代是合适的。关于这一原则应该在指南中进行记录 [问题 23]。
实现缺陷
下面列出了局部实现的缺陷。如果选择这样做,其中大多数可能都可以逐步修复。无论如何,在任何新的 API 设计中都应避免这些缺陷。
有些函数不遵循成功返回 0,失败返回 -1 的约定。例如,PyArg_ParseTuple 成功返回 0,失败返回非零值 [问题 25]。
宏 Py_CLEAR 和 Py_SETREF 多次访问其参数,因此如果参数是带有副作用的表达式,则它们会被重复 [问题 3]。
Py_SIZE 的含义取决于类型,并且并不总是可靠的 [问题 10]。
一些 API 函数的行为与它们的 Python 等价物不同。PyIter_Next 的行为与 tp_iternext 不同。 [问题 29]。PySet_Contains 的行为与 set.__contains__ 不同 [问题 6]。
PyArg_ParseTupleAndKeywords 接受一个非 const char* 数组作为参数,这增加了使用难度 [问题 28]。
Python.h 并未暴露所有 API。某些头文件(如 marshal.h)未从 Python.h 中包含。 [问题 43]。
命名
PyLong 和 PyUnicode 使用的名称不再与它们所代表的 Python 类型 (int/str) 匹配。这可以在新的 API 中修复 [问题 14]。
API 中有些标识符缺少 Py/_Py 前缀 [问题 46]。
缺失功能
本节包含功能请求列表,即当前 C API 中缺失的功能。
调试模式
无需重新编译即可激活的调试模式,该模式会激活各种检查,有助于检测各种类型的错误 [问题 36]。
内省
目前,对于 C 中定义的对象,没有像 Python 对象那样可靠的内省功能 [问题 32]。
堆类型的高效类型检查 [问题 17]。
改进与其他语言的交互
与其他基于 GC 的语言进行接口,并将其 GC 与 Python 的 GC 集成 [问题 19]。
将外部栈帧注入回溯 [问题 18]。
可以在其他语言中使用的具体字符串 [问题 16]。
参考资料
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源:https://github.com/python/peps/blob/main/peps/pep-0733.rst
最后修改时间:2024-10-28 18:52:58 GMT