Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

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 的唯一实现,并且在 CPython 还是唯一实现时做出的一些设计决策,对于替代实现来说难以使用 [问题 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 解释器的应用程序。例如 BlenderOBS

他们需要能够

  • 配置解释器(导入路径、inittab、sys.argv、内存分配器等)。
  • 与执行模型和程序生命周期交互,包括干净的解释器关闭和重新启动。
  • 以 Python 可以使用的方式表示复杂的数据模型,而无需创建深度副本。
  • 提供和导入冻结模块。
  • 运行和管理多个独立的解释器(特别是当嵌入到想要避免全局影响的库中时)。

Python 实现

Python 实现(例如 CPythonPyPyGraalPyIronPythonRustPythonMicroPythonJython)可能会采用非常不同的方法来实现不同的子系统。他们需要

  • API 具有抽象性,并隐藏实现细节。
  • API 的规范,理想情况下,有一个测试套件来确保兼容性。
  • 最好有一个可以在 Python 实现之间共享的 ABI。

替代 API 和绑定生成器

有几个项目实现了 C API 的替代方案,这些方案为扩展用户提供了优于直接使用 C API 的优势。这些 API 使用 C API 实现,在某些情况下,也使用 CPython 内部机制。

还有一些库在 Python 和其他对象模型、范式或语言之间创建绑定。

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

例如,用于 C++ 的 Cythoncffipybind11nanobind,用于 Rust 的 PyO3,PySide 用于 Qt 的 Shiboken,用于 GTK 的 PyGObject,用于 Go 的 Pygolo,用于 Java 的 JPype,用于 Android 的 PyJNIus,用于 Objective-C 的 PyObjC,用于 C/C++ 的 SWIG,用于 .NET (C#) 的 Python.NETHPyMypycPythranpythoncapi-compat。CPython 用于解析函数参数的 DSL,即 Argument Clinic,也可以被视为属于此类利益相关者。

替代 API 需要最少的构建块来高效地访问 CPython。它们不一定需要符合人体工程学的 API,因为它们通常生成的代码并非旨在供人类阅读。但它们确实需要足够全面,以便它们可以避免访问内部机制,而不会牺牲性能。

绑定生成器通常需要

  • 创建自定义对象(例如函数/模块对象和回溯条目),使其行为尽可能接近等效的 Python 代码。
  • 动态创建在传统 C 扩展中是静态的对象(例如类/模块),并需要 CPython 来管理其状态和生命周期。
  • 动态适配外部对象(字符串、GC 托管的容器),并保持低开销。
  • 将外部机制、执行模型和保证适配到 Python 方式(带栈协程、延续、单写多读语义、虚拟多重继承、1 基索引、超长继承链、goroutines、通道等)。

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

C API 的优势

虽然本文档的大部分内容都致力于 C API 中存在的问题,希望在任何新的设计中都能得到解决,但同样重要的是要指出 C API 的优势,并确保这些优势得以保留。

如引言中所述,C API 在过去三十年中促进了 Python 生态系统的开发和发展,同时不断发展以支持其最初设计范围之外的用例。这一记录本身就表明了它的有效性和价值。

capi-workgroup 讨论中提到了许多具体的优势。堆类型被认为比静态类型更安全、更容易使用 [Issue 4]。

基于 Python 字符串进行查找的 API 函数,接受 C 字符串字面量作为参数,非常方便 [Issue 30]。

有限的 API 演示了隐藏实现细节的 API 如何使 Python 更易于演进 [Issue 30]。

C API 问题

本文档的其余部分总结并分类了在 capi-workgroup 存储库中报告的问题。这些问题被分成几个类别。

API 演进和维护

C API 难以进行修改是本报告的核心内容。它隐含在我们讨论的许多问题中,尤其是在我们需要决定增量错误修复是否可以解决问题,或者是否只能作为 API 重设计的一部分来解决时 [Issue 44]。每个增量更改的益处通常被认为太小,不足以证明其带来的中断是合理的。随着时间的推移,这意味着我们在 API 设计或实现中犯下的每一个错误都将无限期地存在。

我们可以从两个角度看待这个问题。一个是认为这是一个问题,并且解决方案需要在任何我们设计的新的 C API 中得到体现,以增量 API 演进流程的形式,其中包括 API 元素的弃用和移除。另一种可能的方法是认为这不是需要解决的问题,而是任何 API 的一个特性。从这个角度来看,API 演进不应该是增量的,而应该是通过大型的重设计,每一次重设计都从过去的错误中吸取教训,并且不受向后兼容性要求的约束(同时,可以添加新的 API 元素,但任何内容都不能被移除)。折衷方案介于这两种极端之间,对容易或足够重要的修复进行增量处理,而其他问题则保持原状。

我们在 CPython 中的问题在于,我们没有一个商定的、正式的 API 演进方法。核心团队的不同成员在不同的方向上努力,这成为了持续的分歧来源。任何新的 C API 都需要明确决定其维护将遵循的模型,以及实现这一目标的技术和组织流程。

如果该模型确实包含了 API 增量演进的条款,它将包括管理更改对用户影响的流程 [Issue 60],也许是通过引入外部向后兼容模块 [Issue 62],或者新的“受认可”函数的 API 层级 [Issue 55]。

API 规范和抽象

C API 没有正式的规范,目前定义为参考实现 (CPython) 在特定版本中包含的内容。文档充当不完整的描述,不足以验证完整 API、有限 API 或稳定 ABI 的正确性。因此,C API 可能会在版本之间发生重大变化,而无需更明显的规范更新,这会导致一系列问题。

除 C/C++ 之外的其他语言的绑定必须解析 C 代码 [Issue 7]。某些 C 语言特性难以以这种方式处理,因为它们会生成编译器相关的输出(例如枚举)或需要 C 预处理器/编译器,而不仅仅是解析器(例如宏) [Issue 35]。

此外,C 头文件往往会暴露超出公共 API 范围的内容 [Issue 34]。特别是,内部数据结构的精确内存布局等实现细节可能会被暴露 [Issue 22PEP 620]。这可能使 API 演进变得非常困难,尤其是在稳定 ABI 中发生时,例如 ob_refcntob_type,它们通过引用计数宏进行访问 [Issue 45]。

我们确定了与引用计数暴露方式相关的更深层次的问题。C 扩展需要使用 Py_INCREFPy_DECREF 调用来管理引用的方式特定于 CPython 的内存模型,并且其他 Python 实现难以模拟。[Issue 12]。

另一组问题源于 PyObject* 在 C API 中作为实际指针而不是句柄暴露的事实。对象的地址充当其 ID 并用于比较,这使得其他在 GC 期间移动对象的 Python 实现变得复杂 [Issue 37]。

一个单独的问题是对象引用对运行时是不透明的,只能通过调用 tp_traverse/tp_clear 来发现,而这些调用有其自身的用途。如果运行时能够知道对象图的结构并跟踪其变化,这将使其他实现能够实现不同的内存管理方案 [Issue 33]。

对象引用管理

函数不存在一致的命名约定,这使得它们的引用语义不明显,并导致容易出错的 C API 函数,因为它们没有遵循典型的行为。当 C API 函数返回 PyObject* 时,调用方通常会获得对该对象的引用的所有权。但是,也有一些例外情况,函数返回“借用”的引用,调用方可以访问该引用,但并不拥有其引用。类似地,函数通常不会更改对其参数的引用的所有权,但也有例外情况,函数“窃取”引用,即引用所有权通过调用从调用方永久转移到被调用方 [Issue 8Issue 52]。文档中用于描述这些情况的术语也可以改进 [Issue 11]。

对于返回“借用”引用(例如 PyList_GetItem) [Issue 5Issue 21] 或指向对象内部结构部分的指针(例如 PyBytes_AsString) [Issue 57] 的函数,需要进行更彻底的更改。在这两种情况下,引用/指针在拥有对象持有引用期间有效,但这段时间难以推理。此类函数不应存在于 API 中,除非存在使其安全的机制。

对于容器,API 目前缺少对包含对象的引用的批量操作。这对于稳定的 ABI 尤其重要,在稳定的 ABI 中 INCREFDECREF 不能是宏,当作为一系列函数调用实现时,批量操作将变得代价高昂 [Issue 15]。

类型定义和对象创建

C API 有一些函数可以创建不完整或不一致的 Python 对象,例如 PyTuple_NewPyUnicode_New。当对象由 GC 跟踪或其 tp_traverse/tp_clear 函数被调用时,这会导致问题。一个相关的问题是使用 PyTuple_SetItem 等函数修改部分初始化的元组(元组一旦完全初始化就不可变) [Issue 56]。

我们确定了类型定义 API 中的一些问题。由于遗留原因,tp_newtp_vectorcall 之间通常存在大量代码重复 [Issue 24]。类型槽函数应间接调用,以便其签名可以更改为包含上下文信息 [Issue 13]。类型定义和创建过程的几个方面没有得到很好的定义,例如该过程的哪个阶段负责初始化和清除类型对象的各个字段 [Issue 49]。

错误处理

C API 中的错误处理基于存储在线程状态(全局范围)中的错误指示器。设计的初衷是每个 API 函数都返回一个值,指示是否发生了错误(按照惯例,为 -1NULL)。当程序知道发生了错误时,它可以获取存储在错误指示器中的异常对象。我们确定了一些与错误处理相关的问题,这些问题指出了 API 太容易被错误使用。

有些函数不会报告在执行过程中发生的所有错误。例如,PyDict_GetItem 会清除在调用键的哈希函数或在字典中执行查找时发生的任何错误 [Issue 51]。

Python 代码永远不会在有正在处理的异常时执行(根据定义),并且通常使用原生函数的代码也应该被引发错误打断。这在大多数 C API 函数中都没有检查,并且在解释器中有一些地方,错误处理代码在设置异常时调用 C API 函数。例如,请参见 _PyErr_WriteUnraisableMsg 的错误处理程序中对 PyUnicode_FromString 的调用 [Issue 2]。

有些函数不返回值,因此调用方必须查询错误指示器以确定是否发生了错误。例如 PyBuffer_Release [Issue 20]。还有一些函数确实有返回值,但此返回值并不能明确指示是否发生了错误。例如,PyLong_AsLong 在发生错误时或参数值确实是 -1 时返回 -1 [Issue 1]。在这两种情况下,API 容易出错,因为在调用函数之前错误指示器可能已经被设置,并且错误被错误地归因。在调用之前未检测到错误是调用代码中的错误,但在这种情况下程序的行为并不能轻松识别和调试问题。

有些函数接受 PyObject* 参数,当其为 NULL 时具有特殊含义。例如,如果 PyObject_SetAttr 接收 NULL 作为要设置的值,则表示应清除该属性。这容易出错,因为 NULL 可能表示值构造中的错误,并且程序未能检查此错误。程序将错误地将 NULL 解释为与错误不同的含义 [Issue 47]。

API 层级和稳定性保证

不同的 API 层级提供了稳定性与 API 演进以及有时性能之间的不同权衡。

稳定的 ABI 被确定为需要研究的领域。目前它是不完整的,并且没有被广泛采用。同时,它的存在使得难以更改一些实现细节,因为它公开了结构体字段,例如 ob_refcntob_typeob_size。关于是否值得保留稳定的 ABI 有一些讨论。双方的论点可以在 [Issue 4] 和 [Issue 9] 中找到。

或者,有人建议为了能够演进稳定的 ABI,我们需要一种机制来支持在同一个 Python 二进制文件中使用多个版本的 ABI。有人指出,在单个 ABI 版本内对单个函数进行版本控制是不够的,因为可能需要同时演进一组相互交互的函数 [Issue 39]。

有限 API 在 3.2 中引入,作为 C API 的一个经过认可的子集,建议用于希望将自己限制在不太可能经常更改的高质量 API 的用户。 Py_LIMITED_API 标志允许用户将其程序限制在旧版本的有限 API 上,但现在我们需要相反的选项来排除旧版本。这将使我们能够通过替换其中有缺陷的元素来演进有限 API [Issue 54]。更一般地说,在重新设计中,我们应该重新审视 API 层级的指定方式,并考虑设计一种方法来统一我们当前在不同层级之间进行选择的方式 [Issue 59]。

名称以下划线开头的 API 元素被认为是私有的,本质上是一个没有稳定性保证的 API 层级。但是,这只是最近在 PEP 689 中得到澄清。对于在 PEP 689 之前存在的此类 API 元素,更改策略应该是什么尚不清楚 [Issue 58]。

有些 API 函数既有非安全(但快速)版本,也有执行错误检查的安全版本(例如,PyTuple_GET_ITEMPyTuple_GetItem)。能够将它们组合到自己的层级中(“非安全 API”层级和“安全 API”层级)可能会有所帮助 [Issue 61]。

C 语言的使用

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

API 中 const 的使用目前很少,但尚不清楚我们是否应该考虑更改这一点 [Issue 38]。

我们目前使用 C 类型 longint,而固定宽度整数(例如 int32_tint64_t)现在可能是更好的选择 [Issue 27]。

我们正在使用 C 语言特性,这些特性难以与其他语言交互,例如宏、可变参数、枚举、位域和非函数符号 [Issue 35]。

有些 API 函数接受 PyObject* 参数,该参数必须是更具体的类型(例如 PyTuple_Size,如果其参数不是 PyTupleObject* 则会失败)。这是否是一个好的模式,或者 API 是否应该期望更具体的类型,这是一个悬而未决的问题 [Issue 31]。

API 中有些函数接受具体类型,例如 PyDict_GetItemString,它执行字典查找以获取指定为 C 字符串而不是 PyObject* 的键。同时,对于 PyDict_ContainsString,添加具体类型替代方案被认为是不合适的。围绕这一点的原则应在指南中记录 [Issue 23]。

实现缺陷

下面是本地化实现缺陷的列表。如果我们选择这样做,大多数这些问题可能都可以逐步修复。无论如何,在任何新的 API 设计中都应该避免这些问题。

有些函数不遵循返回 0 表示成功和 -1 表示失败的约定。例如,PyArg_ParseTuple 返回 0 表示成功,返回非零表示失败 [Issue 25]。

Py_CLEARPy_SETREF 多次访问其参数,因此如果参数是带有副作用的表达式,则会重复 [Issue 3]。

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

一些 API 函数的行为与其 Python 等价物不同。PyIter_Next 的行为不同于 tp_iternext [Issue 29]。PySet_Contains 的行为不同于 set.__contains__ [Issue 6]。

PyArg_ParseTupleAndKeywords 接受非 const char* 数组作为参数,这使得它更难以使用 [Issue 28]。

Python.h 没有公开整个 API。一些头文件(如 marshal.h)没有从 Python.h 中包含 [Issue 43]。

命名

PyLongPyUnicode 使用的名称不再与它们表示的 Python 类型匹配(int/str)。这可以在新的 API 中修复 [Issue 14]。

API 中的一些标识符缺少 Py/_Py 前缀 [Issue 46]。

缺少的功能

本节包含功能请求列表,即在当前 C API 中被识别为缺失的功能。

调试模式

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

内省

目前没有像 Python 对象那样,为用 C 定义的对象提供可靠的内省功能 [Issue 32]。

对堆类型进行高效的类型检查 [Issue 17]。

改进与其他语言的交互

与其他基于 GC 的语言交互,并将它们的 GC 与 Python 的 GC 集成 [Issue 19]。

将外部栈帧注入到回溯中 [Issue 18]。

可以在其他语言中使用的具体字符串 [Issue 16]。

参考文献

  1. Python/C API 参考手册
  2. 2023 年语言峰会博客文章:关于 C API 的三个演讲
  3. GitHub 上的 capi-workgroup
  4. Irit 关于 C API 工作组的 2023 年核心冲刺幻灯片
  5. Petr 的 2023 年核心冲刺幻灯片
  6. HPy 团队关于从 HPy 中学习的 2023 年核心冲刺幻灯片
  7. Victor 关于 2023 年核心冲刺 Python C API 演讲的幻灯片
  8. Python 的稳定性承诺——Cristián Maureira-Fredes,PySide 维护者
  9. 5 年前 PySide 切换到稳定 ABI 时遇到的问题的报告

来源:https://github.com/python/peps/blob/main/peps/pep-0733.rst

上次修改时间:2023-11-14 11:00:51 GMT