PEP 733 – 对 Python 公共 C API 的评估

猫勺猫勺 03-24 147 阅读 0 评论

抽象

此信息性 PEP 描述了我们对公共 C API 的共享视图。这 文档定义:

C API 的用途

利益相关者及其特定用例和要求

C API 的优势

C API 的问题分为九个薄弱环节

本文档不对任何已确定的问题提出解决方案。由 创建 C API 问题的共享列表,本文档将有助于指导 继续讨论变更建议并确定评估 标准。

介绍

Python 的 C API 不是为它目前的不同目的而设计的 满足。它从最初的内部 API 演变而来 解释器的 C 代码以及 Python 语言和库。 在它的第一个化身中,它被暴露出来,使其可以嵌入 将 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 之间的内部接口 interpreter 和 Python 层。后来被曝光为 第三方开发人员扩展和嵌入 Python 的一种方式 程序。多年来,出现了新型的利益相关者, 具有不同的要求和重点领域。本节 用 不同利益相关者需要执行的行动 C API。

所有利益攸关方的共同行动

有些操作是通用的,并且需要 所有类型的 API 用户:

  • 定义函数并调用它们

  • 定义新类型

  • 创建内置类型和用户定义类型的实例

  • 对对象实例执行操作

  • 内省对象,包括类型、实例和函数

  • 引发和处理异常

  • 导入模块

  • 访问 Python 的操作系统界面

以下各节着眼于各种利益相关者的独特要求。

扩展编写器

扩展编写者是 C API 的传统用户。他们的要求 是上面列出的常见操作。他们通常还需要:

  • 创建新模块

  • 在 C 级模块之间高效连接

嵌入式 Python 应用程序的作者

具有嵌入式 Python 解释器的应用程序。例如 Blender 和 OBS。

他们需要能够:

  • 配置解释器(import paths、inittab、 memory 分配器等)。sys.argv

  • 与执行模型和程序生命周期进行交互,包括 清理解释器关闭并重新启动。

  • 以 Python 可以使用的方式表示复杂的数据模型,而无需 必须创建深度副本。

  • 提供和导入冻结模块。

  • 运行和管理多个独立的解释器(特别是当 嵌入到想要避免全局效应的库中)。

Python 实现

Python 实现,例如 CPython、PyPy、GraalPy、IronPython、RustPython、MicroPython、 和 Jython),可以采取 实施方法非常不同 不同的子系统。他们需要:

  • 要抽象并隐藏实现细节的 API。

  • API 的规范,最好是带有测试套件 确保兼容性。

  • 如果有一个可以共享的 ABI 就好了 跨 Python 实现。

替代 API 和绑定生成器

有几个项目实施了替代 C API,为扩展用户提供编程优势 直接使用 C API。这些 API 是通过 C API,在某些情况下使用 CPython 内部。

还有一些库可以在 Python 和 其他对象模型、范式或语言。

这些类别之间存在重叠: 绑定生成器 通常提供替代 API,反之亦然。

例如用于 C++ 的 Cython、cffi、pybind11 和 nanobind、用于 Rust 的 PyO3、用于 用于 Qt 的 PySide、用于 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'd 容器),使用 开销低。

  • 调整外部机制、执行模型和保证 Python 方式(堆栈协程、延续、 单写入器或多读数语义、虚拟多重继承、 基于 1 的索引、超长继承链、goroutine、通道、 等)。

这些工具也可能受益于在更稳定之间进行选择 以及更快(可能更低级别)的 API。他们的用户可以 然后决定他们是否有能力经常重新生成代码,或者 牺牲一些性能来换取更高的稳定性和更少的维护工作。

C API 的优势

虽然本文档的大部分内容都致力于解决 我们希望在任何新设计中看到修复的 C API,它是 指出 C API 的优势也很重要,并且 确保它们被保存下来。

如简介中所述,C API 启用了 Python生态系统的发展和壮大 三十年,同时不断发展以支持用例 最初不是为。这个记录本身就是 表明它的有效性和价值。

在《公约》中提到了一些具体优势。 CAPI-工作组讨论。已识别堆类型 与静态类型一样安全、易于使用 [问题 4]。

采用 C 字符串文本进行查找的 API 函数 在Python字符串上非常方便 [第 30 期]。

受限 API 演示隐藏实现的 API 细节使 Python 的发展更容易 [第 30 期]。

C API 问题

本文档的其余部分对报告的问题进行总结和分类 capi-workgroup 存储库。 这些问题分为几类。

API 演进与维护

在 C API 中进行更改的难度是本报告的核心。是的 隐含在我们在这里讨论的许多问题中,特别是当我们需要时 确定增量 bugfix 是否可以解决问题,或者是否可以解决问题 仅作为 API 重新设计的一部分进行处理 [第 44 期]。这 每个渐进式变化的好处通常被认为太小,无法证明 中断。随着时间的流逝,这意味着我们在 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 中,如 和 , 可通过引用计数宏访问 [第 45 期]。ob_refcntob_type

我们发现了一个更深层次的问题,即与引用方式有关 计数是公开的。需要 C 扩展的方式 使用调用 和 IS 来管理引用 特定于 CPython 的内存模型,并且很难替代 要模拟的 Python 实现。 [第 12 期]。Py_INCREFPy_DECREF

另一组问题源于以下事实:a 是 在 C API 中公开为实际指针,而不是句柄。这 对象的地址用作其 ID,用于比较, 这使得替代 Python 实现的问题变得复杂 在 GC 期间移动对象 [第 37 期]。PyObject*

另一个问题是对象引用对运行时是不透明的, 只能通过调用 / 来发现, 它们有自己的目的。如果有办法让运行时 了解对象图的结构,并跟上其中的变化, 这将使替代实现成为可能 不同的内存管理方案 [第 33 期]。tp_traversetp_clear

对象引用管理

函数不存在一致的命名约定 这使得它们的引用语义显而易见,这导致了 容易出错的 C API 函数,它们不遵循典型的 行为。当 C API 函数返回 时, 调用方通常获得对对象的引用的所有权。 但是,也有一些例外,函数返回 “借用”引用,调用方可以访问但不拥有该引用 引用。同样,函数通常不会更改 对他们论点的引用的所有权,但有 函数“窃取”引用的异常,即 参考文献的所有权永久转移自 呼叫者通过呼叫向被呼叫者 [第 8 期和第 52 期]。 文档中用于描述这些情况的术语 也可以改进 [第 11 期]。PyObject*

对于以下功能,需要进行更彻底的更改 返回“借用”引用(例如) [第 5 期和第 21 期] 或指向对象内部结构部分的指针 (如) [第 57 期]。 在这两种情况下,引用/指针的有效期只要 拥有对象持有引用,但这次很难推理。 如果没有可以 确保他们的安全。PyList_GetItemPyBytes_AsString

对于容器,API 当前缺少对 包含对象的引用。这对于 一个稳定的 ABI,其中不能是宏,使 批量操作在作为函数序列实现时成本高昂 调用 [第 15 期]。INCREFDECREF

类型定义和对象创建

C API 具有可以创建不完整的函数 或不一致的 Python 对象,例如 和 .这会导致在跟踪对象时出现问题 由 GC 或其 / 函数调用。 一个相关的问题是用于修改部分初始化的元组(元组 一旦完全初始化,就不可变) [第 56 期]。PyTuple_NewPyUnicode_Newtp_traversetp_clearPyTuple_SetItem

我们发现类型定义 API 存在一些问题。对于传统 原因,往往有大量的代码重复 在和 [第 24 期] 之间。 类型槽函数应该间接调用,以便它们的 签名可以更改以包含上下文信息 [第 13 期]。 类型定义和创建过程的几个方面不是 定义明确,例如流程的哪个阶段负责 初始化和清除类型对象的不同字段 [第 49 期]。tp_newtp_vectorcall

错误处理

C API 中的错误处理基于存储的错误指示器 在线程状态(在全局范围内)。设计意图是每个 API 函数返回一个值,指示是否发生了错误(由 约定,或 )。当程序知道错误时 发生,它可以获取存储在 错误指示器。我们发现了一些相关的问题 到错误处理,指向太容易错误使用的 API。-1NULL

有些函数不会报告它们时发生的所有错误 执行。例如,清除发生的任何错误 当它调用密钥的哈希函数时,或在执行查找时 在字典中 [第 51 期]。PyDict_GetItem

Python 代码永远不会在运行中异常的情况下执行(根据定义), 通常,使用本机函数的代码也应该被打断 引发错误。这在大多数 C API 函数中都不会检查,并且 解释器中的某些地方,错误处理代码调用 C API 函数。例如,请参阅 [问题 2] 的错误处理程序中的调用。PyUnicode_FromString_PyErr_WriteUnraisableMsg

有些函数不返回值,因此调用者被迫返回 查询错误指示器,以确定是否发生了错误。 一个例子是 [问题 20]。 还有其他函数确实具有返回值,但此返回值 不会明确指示是否发生了错误。例如,在出现错误的情况下返回,或者当 论点确实是[问题 1]。 在这两种情况下,API 都容易出错,因为 在调用函数之前已经设置了错误指示器,并且 错误归因于错误。未检测到错误的事实 调用前是调用代码中的一个错误,但 在这种情况下,程序不容易识别和调试 问题。PyBuffer_ReleasePyLong_AsLong-1-1

有些函数具有特殊含义的参数 当它是.例如,如果接收为 要设置的值,这意味着应清除该属性。这是错误的 容易,因为它可能表明结构中存在错误 的值,并且程序无法检查此错误。该计划将 误解 to 的意思是与错误不同的东西 [第 47 期]。PyObject*NULLPyObject_SetAttrNULLNULLNULL

API 层和稳定性保证

不同的 API 层提供了不同的稳定性与稳定性的权衡 API 演进,有时还有性能。

稳定的ABI被确定为需要研究的领域。在 它不完整且未被广泛采用的那一刻。同时,其 存在使得很难对某些实现进行更改 细节,因为它公开了 struct 字段,例如 和 。关于是否 稳定的ABI值得保留。双方的争论可以是 在 [问题 4] 中找到 和 [问题 9]。ob_refcntob_typeob_size

或者,有人建议,为了能够进化 稳定的 ABI,我们需要一个机制来支持多个版本的 它在同一个 Python 二进制文件中。有人指出,版本控制 单个 ABI 版本中的单个功能是不够的 因为可能需要一起发展一组功能 相互操作 [第 39 期]。

受限 API 是在 3.2 中引入的,是 C API 的一个受祝福的子集 推荐给想要限制自己的用户 到不太可能经常更改的高质量 API。该标志允许用户将其程序限制为较旧的程序 受限 API 的版本,但我们现在需要相反的选择,以 排除旧版本。这将使 通过替换其中有缺陷的元素来限制 API [第 54 期]。 更一般地说,在重新设计中,我们应该重新审视 API 的方式 指定了层,并考虑设计一种方法,将 我们目前在不同层之间进行选择的方式 [第 59 期]。Py_LIMITED_API

考虑名称以下划线开头的 API 元素 私有,本质上是一个没有稳定性保证的 API 层。 然而,直到最近,在PEP 689中才澄清了这一点。是的 不清楚关于此类的变更政策应该是什么 早于 PEP 689 的 API 元素 [第 58 期]。

有些 API 函数具有不安全(但快速)的版本以及 执行错误检查的安全版本(例如,与 )。它可能会有所帮助 能够将它们分组到自己的层中 - “不安全的 API”层和 “安全 API”层 [第 61 期]。PyTuple_GET_ITEMPyTuple_GetItem

C语言的使用

关于CPython的方式提出了一些问题 使用 C 语言。首先是哪种C方言的问题 我们使用,以及我们如何测试我们与它的兼容性,以及 API 标头与 C++ 方言的兼容性 [第 42 期]。

在 API 中的使用目前很少,但事实并非如此 明确这是否是我们应该考虑改变的事情 [第 38 期]。const

我们目前使用 C 类型和 ,其中固定宽度整数 例如,现在可能是更好的选择 [第 27 期]。longintint32_tint64_t

我们正在使用 C 语言功能,这对于其他语言来说很难 交互,例如宏、可变参数、枚举、位域、 和非功能符号 [第 35 期]。

有些 API 函数采用必须是 更具体的类型(例如,如果 它的 arg 不是 )。这是否是一个悬而未决的问题 是一个很好的模式,或者 API 是否应该期望 更具体的类型 [第 31 期]。PyObject*PyTuple_SizePyTupleObject*

API 中有一些函数采用具体类型,例如对键执行字典查找的函数 指定为 C 字符串而不是 .同时, 因为它被认为不合适 添加混凝土类型备选方案。围绕这一点的原则应该 记录在指南中 [问题 23]。PyDict_GetItemStringPyObject*PyDict_ContainsString

实施缺陷

下面是本地化实现缺陷的列表。其中大多数可以 如果我们选择这样做,可能会逐步修复。他们应该, 在任何情况下,在任何新的 API 设计中都应避免使用。

有些函数不遵循约定 成功和失败都回来了。为 例如,返回 0 表示成功,并且 非零表示失败 [第 25 期]。0-1PyArg_ParseTuple

宏并访问它们的 arg 超过 一次,所以如果 arg 是一个有副作用的表达式,它们是 重复 [问题 3]。Py_CLEARPy_SETREF

的含义取决于类型,并不总是 可靠 [第 10 期]。Py_SIZE

某些 API 函数的行为与其 Python 不同 等价物。的行为与 不同。 [第 29 期]。 的行为与 [问题 6] 不同。PyIter_Nexttp_iternextPySet_Containsset.__contains__

将非常量数组作为参数的事实使其更难使用 [第 28 期]。PyArg_ParseTupleAndKeywordschar*

Python.h不会公开整个 API。一些标题(如) 不包括在 中。 [第 43 期]。marshal.hPython.h

命名

PyLong并使用不再与 Python 匹配的名称 它们表示的类型 (/)。这可以在新的 API 中修复 [问题 14]。PyUnicodeintstr

API 中缺少 / 前缀的标识符 [第 46 期]。Py_Py

缺少功能

本部分由功能请求列表组成,即功能 在当前 C API 中被确定为缺失。

调试模式

一种无需重新编译即可激活的调试模式,并且 激活各种检查,以帮助检测各种类型的错误 [第 36 期]。

内省

目前没有针对对象的可靠自省功能 在 C 中定义的方式与 Python 对象的定义相同 [问题 32]。

堆类型的高效类型检查 [第 17 期]。

改进了与其他语言的交互

与其他基于 GC 的语言连接,并集成其 GC 与 Python 的 GC [第 19 期]。

将外部堆栈帧注入回溯 [第 18 期]。

可用于其他语言的具体字符串 [第 16 期]。

版权

本文档位于公共领域或 CC0-1.0-通用许可证,以更宽松者为准。

The End 微信扫一扫

文章声明:以上内容(如有图片或视频在内)除非注明,否则均为腾龙猫勺儿原创文章,转载或复制请以超链接形式并注明出处。

本文作者:猫勺本文链接:https://www.jo6.cn/post/74.html

上一篇 下一篇

相关阅读

发表评论

访客 访客
快捷回复: 表情:
评论列表 (暂无评论,147人围观)

还没有评论,来说两句吧...

取消
微信二维码
微信二维码
支付宝二维码