PEP 697 – 扩展不透明类型的受限 C API
- 作者:
- Petr Viktorin <encukou at gmail.com>
- 讨论至:
- Discourse 帖子
- 状态:
- 最终版
- 类型:
- 标准跟踪
- 创建日期:
- 2022年8月23日
- Python 版本:
- 3.12
- 发布历史:
- 2022年5月24日, 2022年10月6日
- 决议:
- Discourse 消息
摘要
通过允许代码只处理特定(子)类的数据,为用不透明数据扩展某些类型添加了 受限 C API 支持。
此机制需要可与 PyHeapTypeObject 一起使用。
本 PEP 不提议允许扩展非动态大小的可变大小对象,例如 tuple 或 int,因为它们有不同的内存布局,并且目前认为没有这样的需求。本 PEP 为将来在需要时通过相同的机制留下了空间。
动机
本 PEP 解决的激励性问题是将 C 级状态附加到自定义类型——即元类(type 的子类)。
在将另一个类型系统(例如 C++、Java、Rust)公开为 Python 类时,“包装器”通常需要这样做。这些通常需要将有关“包装”的非 Python 类的信息附加到 Python 类型对象。
这应该可以在 Limited API 中完成,以便语言包装器或代码生成器可用于创建稳定的 ABI 扩展。(有关提供稳定 ABI 的好处,请参阅 PEP 652。)
扩展 type 是一个更普遍问题的实例:在保持松散耦合的同时扩展一个类——即不依赖于超类使用的内存布局。(这有很多行话;有关扩展 list 的具体示例,请参阅“基本原理”。)
基本原理
扩展不透明类型
在 Limited API 中,大多数 struct 都是不透明的:它们的尺寸和内存布局不暴露,因此可以在 CPython 的新版本(或 C API 的替代实现)中更改。
这意味着通常的子类化模式——将用于基类型实例的 struct 作为用于派生类型实例的 struct 的第一个元素——不起作用。为了用代码说明,教程中的示例 使用以下 struct 扩展了 PyListObject (list)
typedef struct {
PyListObject list;
int state;
} SubListObject;
这在 Limited API 中将无法编译,因为 PyListObject 是不透明的(以允许在实现功能和优化时进行更改)。
相反,本 PEP 建议使用一个只包含子类所需状态的 struct,即
typedef struct {
int state;
} SubListState;
// (or just `typedef int SubListState;` in this case)
子类现在可以与超类的内存布局(和大小)完全解耦。
这在今天已经可能。要使用这样的结构,请执行以下操作:
- 创建类时,使用
PyListObject->tp_basicsize + sizeof(SubListState)作为PyType_Spec.basicsize; - 访问数据时,使用
PyListObject->tp_basicsize作为实例(PyObject*)的偏移量。
然而,这有缺点
- 基类的
basicsize可能没有正确对齐,如果未缓解,可能导致某些架构上出现问题。(如果对齐在新版本中发生变化,这些问题可能特别棘手。) PyTypeObject.tp_basicsize未在 Limited API 中公开,因此支持 Limited API 的扩展需要使用PyObject_GetAttrString(obj, "__basicsize__")。这很麻烦,并且在极端情况下不安全(Python 属性可能被覆盖)。- 可变大小对象未处理(参见下面的扩展可变大小对象)。
为了使其易于使用(甚至对于选择松散耦合而非最大性能的项目而言成为最佳实践),本 PEP 提出了一个 API 来
- 在类创建期间,指定
SubListState应该“附加”到PyListObject,而无需传递有关list的任何其他详细信息。(解释器本身从基类获取所有必要信息,例如tp_basicsize。)这将通过负的
PyType_Spec.basicsize指定:-sizeof(SubListState)。 - 给定一个实例和子类
PyTypeObject*,获取指向SubListState的指针。为此将添加一个新函数PyObject_GetTypeData。
基类当然不限于 PyListObject:它可以用于扩展任何基类,其实例 struct 是不透明的、在不同版本之间不稳定或根本未公开的——包括 type (PyHeapTypeObject) 或第三方扩展(例如 NumPy 数组 [1])。
对于不需要额外状态的情况,允许零 basicsize:在这种情况下,将继承基类的 tp_basicsize。(这目前有效,但缺乏明确的文档和测试。)
新类的 tp_basicsize 将设置为计算出的总大小,因此检查类的代码将继续像以前一样工作。
扩展可变大小对象
子类化 可变大小对象 时需要额外的考虑,同时保持松散耦合:可变大小数据可能与子类数据(上例中的 SubListState)冲突。
目前,CPython 没有提供防止此类冲突的方法。因此,扩展不透明类的建议机制(负 base->tp_itemsize)默认将失败。
我们可以止步于此,但是由于激励类型——PyHeapTypeObject——是可变大小的,我们需要一种安全的方法来允许对其进行子类化。首先介绍一些背景知识:
可变大小布局
可变大小对象主要有两种内存布局。
在像 int 或 tuple 这样的类型中,可变数据存储在固定偏移量处。如果子类需要额外的空间,它必须在任何可变大小数据之后添加
PyTupleObject:
┌───────────────────┬───┬───┬╌╌╌╌┐
│ PyObject_VAR_HEAD │var. data │
└───────────────────┴───┴───┴╌╌╌╌┘
tuple subclass:
┌───────────────────┬───┬───┬╌╌╌╌┬─────────────┐
│ PyObject_VAR_HEAD │var. data │subclass data│
└───────────────────┴───┴───┴╌╌╌╌┴─────────────┘
在其他类型中,例如 PyHeapTypeObject,可变大小数据始终位于实例内存区域的末尾
heap type:
┌───────────────────┬──────────────┬───┬───┬╌╌╌╌┐
│ PyObject_VAR_HEAD │Heap type data│var. data │
└───────────────────┴──────────────┴───┴───┴╌╌╌╌┘
type subclass:
┌───────────────────┬──────────────┬─────────────┬───┬───┬╌╌╌╌┐
│ PyObject_VAR_HEAD │Heap type data│subclass data│var. data │
└───────────────────┴──────────────┴─────────────┴───┴───┴╌╌╌╌┘
第一个布局可以快速访问项数组。第二个布局允许子类忽略可变大小数组(假设它们使用从对象开始处的偏移量来访问其数据)。
由于本 PEP 关注 PyHeapTypeObject,它提出了一个 API,允许对第二种变体进行子类化。对第一种变体的支持可以在以后作为API 兼容的更改添加(尽管 PEP 作者怀疑这是否值得付出努力)。
使用 PyHeapTypeObject 类似布局扩展类
本 PEP 提出了一个类型标志 Py_TPFLAGS_ITEMS_AT_END,它将指示 PyHeapTypeObject 类似的布局。这可以通过两种方式设置
- 超类可以设置该标志,允许子类作者无需关心
itemsize的参与,或者 - 新的子类设置该标志,表明作者知道超类是合适的(但可能尚未更新以使用该标志)。
此标志将是使用负 basicsize 扩展可变大小类型所必需的。
除了使用标志之外,另一种方法是要求子类作者知道基类使用了兼容的布局(例如,从文档中得知)。本 PEP 的早期版本为此提出了一个新的 PyType_Slot。事实证明这很难解释,并且与将子类与基类布局解耦的想法相悖。
新标志将用于安全地扩展可变大小类型:使用 spec->basicsize < 0 和 base->tp_itemsize > 0 创建类型将需要该标志。
此外,本 PEP 提出了一个辅助函数,用于获取给定实例的可变大小数据,如果它使用新的 Py_TPFLAGS_ITEMS_AT_END 标志。这将在 API 后面隐藏必要的指针算术,该 API 将来可能适应其他布局(包括,可能,VM 管理的布局)。
宏观视图
为了更容易验证所有情况都已涵盖,这里有一个看起来吓人的宏观决策树。
注意
个别情况更容易单独解释(请参阅参考实现以获取草案文档)。
spec->basicsize > 0:现状不变。(基类布局已知。)spec->basicsize == 0:(继承 basicsize)base->tp_itemsize == 0:项目大小设置为spec->tp_itemsize。(现状不变。)base->tp_itemsize > 0:(扩展可变大小类)spec->itemsize == 0:项目大小已继承。(现状不变。)spec->itemsize > 0:项目大小已设置。(这很难安全使用,但这是 CPython 当前的行为。)
spec->basicsize < 0:(扩展 basicsize)base->tp_itemsize == 0:(扩展固定大小类)spec->itemsize == 0:项目大小设置为 0。spec->itemsize > 0:失败。(我们需要添加一个ob_size,这仅适用于平凡类型——并且必须知道平凡布局。)
base->tp_itemsize > 0:(扩展可变大小类)spec->itemsize == 0:(继承 itemsize)- 使用
Py_TPFLAGS_ITEMS_AT_END:继承 itemsize。 - 未使用
Py_TPFLAGS_ITEMS_AT_END:失败。(可能存在冲突。)
- 使用
spec->itemsize > 0:失败。(无法安全地更改/扩展 item size。)
设置 spec->itemsize < 0 始终是错误。本 PEP 不提议任何机制来扩展 tp->itemsize,而只是继承它。
相对成员偏移
难题的另一部分是 PyMemberDef.offset。使用特定于子类的 struct(上面的 SubListState)的扩展将能够指定“相对”偏移量(基于此 struct 的偏移量),而不是“绝对”偏移量(基于 PyObject struct)。
一种方法是在使用新 API 创建类时自动假定“相对”偏移量。然而,这种隐式假设会过于令人惊讶。
为了更明确,本 PEP 提出了一个用于“相对”偏移量的新标志。至少在最初,此标志将仅用于检查误用(并为审阅者提供提示)。如果与新 API 一起使用,则必须存在此标志,否则不得使用此标志。
规范
在下面的代码块中,只有函数头是规范的一部分。其他代码(大小/偏移量计算)是初始 CPython 实现的细节,可能会发生变化。
相对 basicsize
PyType_Spec 的 basicsize 成员将允许为零或负数。在这种情况下,它的绝对值将指定新类的实例除了基类的 basicsize 之外所需的额外存储空间大小。也就是说,结果类的 basicsize 将是
type->tp_basicsize = _align(base->tp_basicsize) + _align(-spec->basicsize);
其中 _align 向上舍入到 alignof(max_align_t) 的倍数。
当 spec->basicsize 为零时,basicsize 将直接继承,即设置为 base->tp_basicsize 而不进行对齐。(这已经有效;将添加显式测试和文档。)
在实例上,特定于子类的内存区域——即子类除了其基类之外保留的“额外空间”——将通过一个新函数 PyObject_GetTypeData 获得。在 CPython 中,此函数将定义为
void *
PyObject_GetTypeData(PyObject *obj, PyTypeObject *cls) {
return (char *)obj + _align(cls->tp_base->tp_basicsize);
}
将添加另一个函数以检索此内存区域的大小
Py_ssize_t
PyType_GetTypeDataSize(PyTypeObject *cls) {
return cls->tp_basicsize - _align(cls->tp_base->tp_basicsize);
}
结果可能高于 -basicsize 请求的值。使用所有这些值是安全的(例如,使用 memset)。
新的 *Get* 函数带有一个重要的注意事项,这将在文档中指出:它们只能用于使用负 PyType_Spec.basicsize 创建的类。对于其他类,它们的行为是未定义的。(请注意,这允许上述代码假定 cls->tp_base 不为 NULL。)
继承 itemsize
当 spec->itemsize 为零时,tp_itemsize 将从基类继承。(这已经有效;将添加显式测试和文档。)
将添加一个新的类型标志 Py_TPFLAGS_ITEMS_AT_END。此标志只能在 tp_itemsize 非零的类型上设置。它表示实例的可变大小部分存储在实例内存的末尾。
默认元类型(PyType_Type)将设置此标志。
将添加一个新函数 PyObject_GetItemData,用于访问带有新标志的类型的可变大小内容所保留的内存。在 CPython 中,它将被定义为
void *
PyObject_GetItemData(PyObject *obj) {
if (!PyType_HasFeature(Py_TYPE(obj), Py_TPFLAGS_ITEMS_AT_END) {
<fail with TypeError>
}
return (char *)obj + Py_TYPE(obj)->tp_basicsize;
}
此函数最初不会添加到 Limited API 中。
使用负 spec->basicsize 扩展带有正 base->itemsize 的类将失败,除非设置了 Py_TPFLAGS_ITEMS_AT_END,无论是在基类上还是在 spec->flags 中。(有关完整解释,请参阅扩展可变大小对象。)
使用负 spec->basicsize 扩展带有正 spec->itemsize 的类将失败。
相对成员偏移
在使用负 PyType_Spec.basicsize 定义的类型中,通过 Py_tp_members 定义的成员的偏移量必须相对于额外的子类数据,而不是完整的 PyObject 结构体。这将在 PyMemberDef.flags 中由一个新标志表示:Py_RELATIVE_OFFSET。
在初始实现中,新标志将是冗余的。它仅用于使偏移量的更改含义清晰,并有助于避免错误。不使用带负 basicsize 的 Py_RELATIVE_OFFSET 将是一个错误,而在任何其他上下文(即对 PyDescr_NewMember、PyMember_GetOne、PyMember_SetOne 的直接或间接调用)中使用它也将是一个错误。
CPython 将在初始化类型时调整偏移量并清除 Py_RELATIVE_OFFSET 标志。这意味着
- 创建的类型的
tp_members将与输入定义的Py_tp_members槽不匹配,并且 - 任何读取
tp_members的代码都无需处理该标志。
新 API 列表
提出了以下新的函数/值。
这些将添加到 Limited API/Stable ABI 中
void * PyObject_GetTypeData(PyObject *obj, PyTypeObject *cls)Py_ssize_t PyType_GetTypeDataSize(PyTypeObject *cls)Py_TPFLAGS_ITEMS_AT_END标志用于PyTypeObject.tp_flagsPy_RELATIVE_OFFSET标志用于PyMemberDef.flags
这些将仅添加到公共 C API 中
void *PyObject_GetItemData(PyObject *obj)
向后兼容性
未发现向后兼容性问题。
假设
该实现假设实例的内存位于 type->tp_base->tp_basicsize 和 type->tp_basicsize 偏移量之间“属于” type(可变长度类型除外)。这并未明确记录,但 CPython 3.11 及更早版本在向子类添加 __dict__ 时依赖于它,因此应该安全。
安全隐患
未知。
认可
如何教授此内容
初始实现将包括参考文档和“新增功能”条目,这对于目标受众——C 扩展库的作者——来说应该足够了。
参考实现
参考实现在 encukou/cpython GitHub 存储库的 extend-opaque 分支中。
未来可能的增强
对齐与性能
如果实例结构需要比 alignof(max_align_t) 更小的对齐方式,则拟议的实现可能会浪费一些空间。此外,处理对齐会使计算速度变慢,如果我们能够依赖 base->tp_basicsize 正确对齐子类型,那么计算速度会更快。
换句话说,提议的实现侧重于安全性和易用性,并为此牺牲了空间和时间。如果这被证明是一个问题,可以在不破坏 API 的情况下调整实现
- 类型特定缓冲区的偏移量可以存储起来,这样
PyObject_GetTypeData实际上就变成了(char *)obj + cls->ht_typedataoffset,这可能会以类中多一个指针为代价来加快速度。 - 然后,一个新的
PyType_Slot可以指定所需的对齐方式,以减少实例的空间需求。
可变大小类型的其他布局
可以添加一个类似 Py_TPFLAGS_ITEMS_AT_END 的标志,以指示扩展可变大小对象中描述的“类元组”布局,并且本 PEP 提出的所有机制都可以适应以支持它。也可以添加其他布局。然而,似乎实际收益很小,因此这只是一个理论上的可能性。
被拒绝的想法
可以用一个新的 PyType_Spec 标志代替负的 spec->basicsize。对于任何在没有最新更改知识的情况下访问这些内部的现有代码,其效果将是相同的,因为在此情况下,字段值的含义正在改变。
脚注
版权
本文档置于公共领域或 CC0-1.0-Universal 许可证下,以更宽松者为准。
来源: https://github.com/python/peps/blob/main/peps/pep-0697.rst
最后修改: 2025-02-01 08:55:40 GMT