首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >Python C API统一槽系统:PySlot提案详解

Python C API统一槽系统:PySlot提案详解

原创
作者头像
用户11764306
发布2026-01-31 18:22:52
发布2026-01-31 18:22:52
720
举报

PEP 820 – PySlot: C API统一槽系统

作者:

Petr Viktorin <encukou at gmail.com>

讨论链接:

Discourse thread

状态:

草案

类型:

标准追踪

创建日期:

2025-12-19

Python版本:

3.15

发布历史:

2026-01-06

摘要

用一个新的、类型安全性更高的结构替换类型槽和模块槽,该结构允许以更具前向兼容性的方式添加新槽。现有的槽结构及相关API将被软弃用。(即:它将继续工作且不会产生警告,并将被完整记录和支持,但计划不再为其添加任何新功能。)

背景

Python 3.14的C API包含两个可扩展的结构体,用于在创建新对象时提供信息:PyType_SpecPyModuleDef。每个都有一系列C API函数用于从中创建Python对象。这些是:

  • 用于 PyType_SpecPyType_From* 函数,如 PyType_FromMetaclass()
  • 用于 PyModuleDefPyModule_FromDef* 函数,如 PyModule_FromDefAndSpec()

将“输入”结构与运行时对象分离,使得对象的内部结构可以保持不透明(在API和ABI层面),允许未来的CPython版本(甚至其他实现)更改细节。这两个结构都包含一个 slots 字段——本质上是一个标记联合数组,允许未来扩展。(实际上,槽是使用整数ID标记的void指针。)在PEP 793中,添加了新的模块创建API。它不再使用 PyModuleDef 结构,而是只使用一个槽数组。为了替换 PyModuleDef 的现有成员,它添加了相应的槽ID——例如,模块名称在 Py_mod_name 槽中指定,而不是在 PyModuleDef.m_name 中。该PEP指出:

PyModuleDef_Slot 结构体与固定字段相比确实有一些缺点。我们相信这些是可以修复的,但将此问题留作本PEP的范围之外。本提案旨在解决这些缺点。

动机

现有 PyModuleDef_SlotPyType_Slot 的主要缺点有:

  1. 类型安全性void * 用于数据指针、函数指针和小整数,需要进行强制转换,这在所有相关架构上实践可行,但从技术上讲是C语言中未定义或实现定义的行为。例如:Py_tp_doc 标记一个字符串;Py_mod_gil 标记一个整数;Py_tp_repr 标记一个函数;所有这些都必须强制转换为 void*
  2. 有限的前向兼容性:如果扩展提供的槽ID是当前解释器未知的,类型/模块创建将失败。这使得使用“可选”功能变得麻烦——这些功能应仅在解释器支持它们时才生效。最近添加的槽 Py_mod_gilPy_mod_multiple_interpreters 就是很好的例子。一种解决方法是检查Python版本,并省略早于当前解释器的槽。这对用户来说很麻烦。它也限制了C API可能的非CPython实现,阻止它们“挑选”较新CPython版本中引入的功能。

示例

本提案添加了从槽数组创建类和模块的API,这些槽可以使用宏指定为C字面量,如下所示:

代码语言:c
复制
static PySlot myClass_slots[] = {
   PySlot_STATIC(tp_name, "mymod.MyClass"),
   PySlot_SIZE(tp_extra_basicsize, sizeof(struct myClass)),
   PySlot_FUNC(tp_repr, myClass_repr),
   PySlot_INT64(tp_flags, Py_TPFLAGS_DEFAULT | Py_TPFLAGS_MANAGED_DICT),
   PySlot_END,
};

// ...

PyObject *MyClass = PyType_FromSlots(myClass_slots, -1);

宏简化了手写字面量。对于更复杂的用例,例如多个Python版本之间的兼容性,或模板化/自动生成的槽数组,以及C API的非C用户,可以写出槽结构体定义。例如,如果从 tp_getattrtp_getattro 的过渡发生在不久的将来(比如CPython 3.17),而不是1.4,并且用户想要支持带有和不带有 tp_getattro 的CPython,他们可以添加一个“HAS_FALLBACK”标志:

代码语言:c
复制
static PySlot myClass_slots[] = {
   ...
   {   // 如果不支持则跳过
       .sl_id=Py_tp_getattro,
       .sl_flags=PySlot_HAS_FALLBACK,
       .sl_func=myClass_getattro,
   },
   {    // 如果上面的槽被跳过则使用这个
       .sl_id=Py_tp_getattr,
       .sl_func=myClass_old_getattr,
   },
   PySlot_END,
};

类似地,如果 nb_matrix_multiply 槽(PEP 465)在不久的将来被添加,用户可以添加一个带有“OPTIONAL”标志的槽,使他们的类仅在有该操作符的CPython版本上支持 @ 操作符。

设计原理

这里我们解释本提案中的设计决策。部分原理与PEP 793重复,该PEP用槽数组替换了 PyModuleDef 结构体。

使用槽

槽的主要替代方案是使用版本化结构体作为输入。这种设计有两种变体:

  1. 一个包含所有信息字段的大型结构体。正如我们从 PyTypeObject 中看到的,这种结构体的大部分在实践中往往是NULL。随着更多字段变得过时,要么浪费增长,要么我们引入新的结构体布局(同时在一段时间内保持与旧布局的兼容性)。
  2. 一个仅包含初始创建所需信息的小型结构体,其他信息随后添加(通过专用函数调用或Python级别的setattr)。这种设计使得添加/废弃/调整所需信息变得麻烦;增加了扩展和解释器之间的API调用次数。我们认为,即使它部分重复了修改“活动”对象的API,用于类型/模块创建的“批处理”API也是有意义的。

仅使用槽

PyType_SpecPyModuleDef 除了槽数组外还有显式字段。这些包括:

  • 必需的信息,例如类名(PyType_Spec.name)。本提案为名称添加了一个槽ID,并使其成为必需。
  • 非指针(basicsize, flags)。最初,槽只包含函数指针;它们现在也包含数据指针以及整数或标志。本提案使用联合体来干净地处理类型。
  • 在槽机制之前添加的项目。PyModuleDef.m_slots 本身是从总是为NULL的 m_reload 重新利用而来的;可选的 m_traversem_methods 成员早于它。

我们可以没有这些字段,只保留一个槽数组。围绕数组的包装类会使设计复杂化。如果此类中的字段变得过时,它们很难被移除或重新利用。

嵌套槽表

在本提案中,槽数组可以引用另一个槽数组,后者被视为递归地合并到其“父”数组中。这使解释器内部的槽处理复杂化,但允许:

  • 将动态分配(或栈分配)的槽与静态槽混合。这解决了导致 PyType_From* 系列函数扩展的问题,这些值通常不能是静态的(即在Windows上,它通常是来自另一个DLL的符号,不能是静态数据)。
  • 共享槽的子集以实现多个类/模块通用的功能。
  • 根据Python版本等条件轻松地有条件地包含一些槽。

嵌套“旧式”槽表

类似于嵌套 PyType_Slot 数组,我们还提议在“新”槽中支持“旧式”槽(PyType_SlotPyModuleDef_Slot)的数组,反之亦然。这样,用户可以重用他们已经编写的代码而无需重写/重新格式化,并且仅在需要任何新功能时才使用“新”槽。

固定宽度整数

本提案使用固定宽度整数(uint16_t)作为槽ID和标志。使用C的 int 类型,使用超过16位将不可移植,但在常见平台上会默默地工作。使用 int 但避免超过 UINT16_MAX 的值在常见平台上会浪费16位。将这些定义为 uint16_t 后,除了指针和大小之外的所有内容都使用固定宽度整数似乎是自然的。

内存布局

在常见的64位平台上,我们可以保持新结构体的大小与现有的 PyType_SlotPyModuleDef_Slot 相同。(现有的结构体由于 int 的可移植性和填充而浪费了16个字节中的6个;本提案将这些位用于新功能。)在32位平台上,本提案要求采用与64位相同的布局,大小是现有结构体的两倍(从8字节到16字节)。对于通常是静态的“配置”数据,这应该没问题。该提案不使用位域和枚举,因为它们的内存表示依赖于编译器,在使用C以外的语言使用API时会导致问题。结构的布局假设类型的对齐方式与其大小匹配。

单一ID空间

目前,模块槽和类型槽的数值有重叠:

  • Py_bf_getbuffer == Py_mod_create == 1
  • Py_bf_releasebuffer == Py_mod_exec == 2
  • Py_mp_ass_subscript == Py_mod_multiple_interpreters == 3
  • Py_mp_length == Py_mod_gil == 4
  • 以及在CPython 3.15中添加的模块槽有类似情况

本提案为两者使用单一的序列,因此未来的槽避免这种重叠。这是为了:

  • 避免意外地将类型槽用于模块,反之亦然
  • 允许外部库或检查器根据ID确定槽的含义(和类型)

现有的4个重叠意味着我们现在无法达到这些目标,但我们可以以对用户透明的方式逐渐迁移到新的数字ID。主要缺点是任何内部查找表要么会更大(如果我们为类型和模块使用单独的表,它们将包含空白),要么更难管理(如果它们被合并)。

规范

将定义一个新的 PySlot 结构体如下:

代码语言:c
复制
typedef struct PySlot {
    uint16_t sl_id;
    uint16_t sl_flags;
    union {
        uint32_t _sl_reserved;  // 必须为0
    };
    union {
        void *sl_ptr;
        void (*sl_func)(void);
        Py_ssize_t sl_size;
        int64_t sl_int64;
        uint64_t sl_uint64;
    };
} PySlot;
  • sl_id:一个槽编号,标识槽的作用。
  • sl_flags:标志,定义如下。
  • 为未来扩展保留的32位(预计由未来标志启用)。
  • 一个包含数据的联合体,其类型取决于槽。

使用槽的函数

将添加以下函数。它将从给定的槽数组创建相应的Python类型对象:

代码语言:c
复制
PyObject *PyType_FromSlots(const PySlot *slots);

PyModule_FromSlotsAndSpec 函数(在PEP 793中于CPython 3.15添加)将被更改为接受新的槽结构:

代码语言:c
复制
PyObject *PyModule_FromSlotsAndSpec(const PySlot *slots, PyObject *spec)

通用槽语义

当槽被传递给应用它们的函数时,该函数不会修改槽数组,也不会修改它(递归地)指向的任何数据。函数完成后,允许用户修改或释放数组及其(递归地)指向的任何数据,除非它被明确标记为“静态”(参见下面的 PySlot_STATIC)。这意味着解释器通常需要复制结构体中的所有数据,包括 char * 文本。

标志

sl_flags 可以设置以下位。未分配的位必须设置为零。

  • PySlot_OPTIONAL:如果槽ID未知,解释器应完全忽略该槽。(例如,如果现在正在向CPython添加 nb_matrix_multiply,你的类型可以使用这个。)
  • PySlot_STATIC:槽指向的所有数据都是静态分配且恒定的。因此,解释器不需要复制信息。函数指针隐含此标志。该标志甚至适用于槽“间接”指向的数据,除了嵌套槽——参见下面的 Py_slot_subslots——它们可以有自己的 PySlot_STATIC 标志。例如,如果应用于指向 PyMemberDef 结构数组的 Py_tp_members 槽,则整个数组及其元素中的名称和文档字符串必须是静态且恒定的。
  • PySlot_HAS_FALLBACK:如果槽ID未知,解释器将忽略该槽。如果已知,解释器应忽略后续槽,直到(并包括)第一个没有 HAS_FALLBACK 的槽。实际上,带有 HAS_FALLBACK 标志的连续槽,加上它们之后的第一个非 HAS_FALLBACK 槽,形成一个“块”,解释器将只考虑该块中它理解的第一个槽。如果整个块是可选的,它应该以一个带有 OPTIONAL 标志的槽结束。
  • PySlot_IS_PTR:数据存储在 sl_ptr 中,并且必须强制转换为适当的类型。此标志简化了从现有 PyType_SlotPyModuleDef_Slot 的移植,其中所有槽都以这种方式工作。

便利宏

以下宏将被添加到API中以简化槽定义:

代码语言:c
复制
#define PySlot_DATA(NAME, VALUE) \
   {.sl_id=NAME, .sl_ptr=(void*)(VALUE)}

#define PySlot_FUNC(NAME, VALUE) \
   {.sl_id=NAME, .sl_func=(VALUE)}

#define PySlot_SIZE(NAME, VALUE) \
   {.sl_id=NAME, .sl_size=(VALUE)}

#define PySlot_INT64(NAME, VALUE) \
   {.sl_id=NAME, .sl_int64=(VALUE)}

#define PySlot_UINT64(NAME, VALUE) \
   {.sl_id=NAME, .sl_uint64=(VALUE)}

#define PySlot_STATIC_DATA(NAME, VALUE) \
   {.sl_id=NAME, .sl_flags=PySlot_STATIC, .sl_ptr=(VALUE)}

#define PySlot_END {0}

我们还将添加另外两个避免使用指定初始化器的宏,用于C++11兼容代码:

代码语言:c
复制
#define PySlot_PTR(NAME, VALUE) \
   {NAME, PySlot_IS_PTR, {0}, {(void*)(VALUE)}}

#define PySlot_PTR_STATIC(NAME, VALUE) \
   {NAME, PySlot_IS_PTR|Py_SLOT_STATIC, {0}, {(void*)(VALUE)}}

嵌套槽表

将添加一个新槽 Py_slot_subslots 以允许嵌套槽表。它的值(sl_ptr)应指向一个 PySlot 结构体数组,这些结构体将被视为当前槽数组的一部分。sl_ptr 可以是 NULL 以表示没有槽。另外两个槽将允许对现有槽结构进行类似的嵌套:

  • Py_tp_slots 用于 PyType_Slot 数组
  • Py_mod_slots 用于 PyModuleDef_Slot 数组

数组中的每个 PyType_Slot 将被转换为 (PySlot){.sl_id=slot, .sl_flags=PySlot_IS_PTR, .sl_ptr=func}PyModuleDef_Slot 类似。初始实现将有一些限制,未来可能会取消:

  • Py_slot_subslotsPy_tp_slotsPy_mod_slots 不能使用 PySlot_HAS_FALLBACK(该标志不能在它们身上设置,也不能在它们之前的槽上设置)。
  • 嵌套深度将限制为5层。(对于现有的 PyType_From*PyModule_From* 函数为4层,它们将在内部使用一层。)

新槽ID

将添加以下新的槽ID,可用于类型和模块定义:

  • Py_slot_end(定义为0):标记槽数组的结束。忽略 PySlot_INTPTRPySlot_STATIC 标志。不允许与 Py_slot_end 一起使用 PySlot_OPTIONALPySlot_HAS_FALLBACK 标志。
  • Py_slot_subslotsPy_tp_slotsPy_mod_slots:参见上面的嵌套槽表
  • Py_slot_invalid:被视为未知槽ID。

将添加以下新的槽ID以覆盖 PyModuleDef 的现有成员:

  • Py_tp_name(类型创建必需)
  • Py_tp_basicsize(类型为 Py_ssize_t
  • Py_tp_extra_basicsize(等同于将 PyType_Spec.basicsize 设置为 -extra_basicsize
  • Py_tp_itemsize
  • Py_tp_flags

将添加以下新的槽ID以覆盖 PyType_FromMetaclass 的参数:

  • Py_tp_metaclass(用于在元类计算后设置 ob_type
  • Py_tp_module

注意 Py_tp_basePy_tp_bases 已经存在。解释器将同等对待它们:两者都可以指定一个类对象或它们的元组。Py_tp_base 将被软弃用,以支持 Py_tp_bases。在单个定义中同时指定两者将被弃用(目前,Py_tp_bases 会覆盖 Py_tp_base)。这些新槽都不能与 PyType_GetSlot 一起使用。(此限制未来可能会取消,需经C API工作组批准。)

槽重新编号

新的槽ID将具有唯一的数值(即 Py_slot_*Py_tp_*Py_mod_* 不会共享ID)。编号为1到4的槽(Py_bf_getbufferPy_mp_lengthPy_mod_createPy_mod_gil)将被重新定义为新的(更大的)数字。旧数字将保留为别名,并在为低于3.15的稳定ABI版本编译时使用。PEP 793中添加的用于 PyType_Spec 成员的槽将被重新编号,使它们具有唯一的ID:

  • Py_mod_name
  • Py_mod_doc
  • Py_mod_state_size
  • Py_mod_methods
  • Py_mod_state_traverse
  • Py_mod_state_clear
  • Py_mod_state_free

软弃用

以下现有函数将被软弃用:

  • PyType_FromSpec
  • PyType_FromSpecWithBases
  • PyType_FromModuleAndSpec
  • PyType_FromMetaclass
  • PyModule_FromDefAndSpec
  • PyModule_FromDefAndSpec2
  • PyModule_ExecDef

(提醒:软弃用的API不计划移除,不会引发警告,并保持文档化和测试。但是,不会为其添加新功能。)这些函数接受的 PyType_SlotPyModuleDef_Slot 数组可以包含任何槽,包括本PEP中定义的“新”槽。这包括嵌套的“新式”槽(Py_slot_subslots)。

向后兼容性

本PEP仅添加API,因此是向后兼容的。

安全影响

未知

如何教授

调整“扩展和嵌入”教程以使用此方法。

参考实现

暂无。

被拒绝的想法

暂无。

未决问题

暂无。

版权

本文档置于公共领域或CC0-1.0-Universal许可下,以较宽松者为准。FINISHED

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • PEP 820 – PySlot: C API统一槽系统
    • 摘要
    • 背景
    • 动机
    • 示例
    • 设计原理
    • 规范
    • 向后兼容性
    • 安全影响
    • 如何教授
    • 参考实现
    • 被拒绝的想法
    • 未决问题
    • 版权
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档