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 类型对象。
这应该可以在受限 API 中实现,以便可以使用语言包装器或代码生成器来创建稳定的 ABI 扩展。(有关提供稳定 ABI 的好处,请参阅 PEP 652。)
扩展 type
是一个更普遍问题的一个实例:在保持松耦合的同时扩展类,也就是说,不依赖于超类使用的内存布局。(这有很多术语;请参阅基本原理以获取扩展 list
的具体示例。)
基本原理
扩展不透明类型
在受限 API 中,大多数 struct
都是不透明的:它们的尺寸和内存布局不会公开,因此可以在 CPython 的新版本(或 C API 的替代实现)中更改。
这意味着通常的子类化模式(使用于基类型实例的 struct
成为用于派生类型实例的 struct
的第一个元素)不起作用。为了用代码说明,教程中的示例 使用以下 struct
扩展了 PyListObject
(list
)
typedef struct {
PyListObject list;
int state;
} SubListObject;
这在受限 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
在受限 API 中未公开,因此支持受限 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 可以在未来适应其他布局(包括潜在的虚拟机管理布局)。
全局视角
为了更轻松地验证所有情况都已涵盖,这里有一个看起来很吓人的大图决策树。
注意
各个案例在孤立的情况下更容易解释(有关草稿文档,请参阅参考实现)。
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
:失败。(无法安全地更改/扩展项目大小。)
设置spec->itemsize < 0
始终是错误。此 PEP 没有提出任何扩展tp->itemsize
而不是仅仅继承它的机制。
相对成员偏移量
拼图的另一块是PyMemberDef.offset
。使用子类特定struct
(上面为SubListState
)的扩展将获得一种指定“相对”偏移量(基于此struct
的偏移量)而不是“绝对”偏移量(基于PyObject
结构)的方法。
一种方法是在使用新 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;
}
此函数最初将不会添加到受限 API 中。
使用负spec->basicsize
扩展具有正base->itemsize
的类将失败,除非在基类或spec->flags
中设置了Py_TPFLAGS_ITEMS_AT_END
。(有关完整说明,请参阅扩展可变大小对象。)
使用负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 列表
建议使用以下新函数/值。
这些将添加到受限 API/稳定 ABI 中
void * PyObject_GetTypeData(PyObject *obj, PyTypeObject *cls)
Py_ssize_t PyType_GetTypeDataSize(PyTypeObject *cls)
Py_TPFLAGS_ITEMS_AT_END
用于PyTypeObject.tp_flags
Py_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 扩展库的作者——来说应该足够了。
参考实现
参考实现位于 extend-opaque 分支,位于 encukou/cpython
GitHub 仓库中。
可能的未来增强
对齐和性能
提议的实现可能会浪费一些空间,如果实例结构体需要的对齐方式小于 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-通用许可证,以较宽松者为准。
来源: https://github.com/python/peps/blob/main/peps/pep-0697.rst
上次修改: 2024-08-20 10:29:32 GMT