隔离扩展模块¶
谁应该阅读此文档¶
本指南是为 C-API 扩展的维护者编写的,他们希望使其扩展在 Python 本身用作库的应用程序中更安全地使用。
背景¶
解释器 是 Python 代码运行的上下文。它包含配置(例如导入路径)和运行时状态(例如导入的模块集)。
Python 支持在一个进程中运行多个解释器。有两种情况需要考虑——用户可以运行解释器
按顺序,使用几个
Py_InitializeEx()
/Py_FinalizeEx()
循环,以及并行,使用
Py_NewInterpreter()
/Py_EndInterpreter()
管理“子解释器”。
当将 Python 嵌入到库中时,这两种情况(以及它们的组合)都非常有用。库通常不应该对使用它们的应用程序做出假设,这包括假设进程范围的“主 Python 解释器”。
从历史上看,Python 扩展模块并没有很好地处理这种情况。许多扩展模块(甚至一些 stdlib 模块)使用按进程全局状态,因为 C static
变量非常容易使用。因此,应该特定于解释器的数据最终会在解释器之间共享。除非扩展开发者小心,否则很容易引入边缘情况,导致在同一进程中的多个解释器中加载模块时崩溃。
不幸的是,按解释器状态并不容易实现。扩展作者在开发时往往不考虑多个解释器,而且目前测试行为也很麻烦。
进入按模块状态¶
Python 的 C API 正在发展,以更好地支持更精细的按模块状态,而不是专注于按解释器状态。这意味着 C 级数据应该附加到模块对象。每个解释器都会创建自己的模块对象,保持数据分离。为了测试隔离,甚至可以在单个解释器中加载对应于单个扩展的多个模块对象。
按模块状态提供了一种简单的方法来考虑生命周期和资源所有权:扩展模块将在创建模块对象时初始化,并在释放时清理。在这方面,模块就像任何其他 PyObject* 一样;无需考虑或忘记“在解释器关闭时”挂钩。
请注意,存在各种“全局变量”的用例:按进程、按解释器、按线程或按任务状态。以按模块状态作为默认值,这些仍然是可能的,但您应该将它们视为特殊情况:如果您需要它们,您应该对它们进行额外的注意和测试。(请注意,本指南不涵盖它们。)
隔离的模块对象¶
开发扩展模块时要记住的关键点是,可以从单个共享库创建多个模块对象。例如
>>> import sys
>>> import binascii
>>> old_binascii = binascii
>>> del sys.modules['binascii']
>>> import binascii # create a new module object
>>> old_binascii == binascii
False
根据经验,这两个模块应该是完全独立的。模块特定的所有对象和状态都应封装在模块对象中,而不是与其他模块对象共享,并在模块对象被释放时清理。由于这只是一条经验法则,因此例外情况是可能的(请参阅 管理全局状态),但它们将需要更多的思考和对边缘情况的关注。
虽然有些模块可以使用较少的严格限制,但隔离的模块更容易设置明确的期望和适用于各种用例的指导方针。
令人惊讶的边缘情况¶
请注意,隔离的模块确实会创建一些令人惊讶的边缘情况。最值得注意的是,每个模块对象通常不会与其他类似模块共享其类和异常。继续 上面的示例,请注意 old_binascii.Error
和 binascii.Error
是单独的对象。在以下代码中,异常没有被捕获
>>> old_binascii.Error == binascii.Error
False
>>> try:
... old_binascii.unhexlify(b'qwertyuiop')
... except binascii.Error:
... print('boo')
...
Traceback (most recent call last):
File "<stdin>", line 2, in <module>
binascii.Error: Non-hexadecimal digit found
这是预期的。请注意,纯 Python 模块的行为方式相同:这是 Python 工作方式的一部分。
目标是使扩展模块在 C 级别上安全,而不是使 hack 行为直观。 “手动”修改 sys.modules
算作 hack。
使模块在多个解释器中安全¶
管理全局状态¶
有时,与 Python 模块关联的状态不是特定于该模块,而是特定于整个进程(或其他比模块“更全局”的东西)。例如
readline
模块管理终端。在电路板上运行的模块想要控制板载 LED。
在这些情况下,Python 模块应该提供对全局状态的访问,而不是拥有它。如果可能,请编写模块,以便它的多个副本可以独立访问该状态(以及其他库,无论是用于 Python 还是其他语言)。如果不可能,请考虑显式锁定。
如果必须使用进程全局状态,避免多个解释器问题的最简单方法是显式地阻止每个进程多次加载模块——请参阅 选择退出:限制每个进程一个模块对象。
管理按模块状态¶
要使用按模块状态,请使用 多阶段扩展模块初始化。这表示您的模块正确支持多个解释器。
将 PyModuleDef.m_size
设置为正数,以请求该模块本地的许多字节的存储空间。通常,这将设置为某个模块特定的 struct
的大小,它可以存储模块的所有 C 级状态。特别是,您应该将指向类(包括异常,但不包括静态类型)和设置(例如 csv
的 field_size_limit
)的指针放在这里,C 代码需要这些指针才能运行。
注意
另一种选择是将状态存储在模块的 __dict__
中,但您必须避免在用户从 Python 代码修改 __dict__
时崩溃。这通常意味着在 C 级别进行错误和类型检查,这很容易出错且难以充分测试。
然而,如果 C 代码中不需要模块状态,将其仅存储在 __dict__
中是一个好主意。
如果模块状态包含 PyObject
指针,则模块对象必须持有对这些对象的引用,并实现模块级别的钩子 m_traverse
、m_clear
和 m_free
。这些钩子的工作方式类似于类的 tp_traverse
、tp_clear
和 tp_free
。添加它们会需要一些工作并使代码更长;这是为了使模块可以被干净卸载所付出的代价。
目前有一个带有每个模块状态的模块示例,位于 xxlimited;示例模块初始化代码显示在文件底部。
选择退出:限制每个进程只有一个模块对象¶
一个非负的 PyModuleDef.m_size
表示一个模块正确支持多个解释器。如果您的模块尚未支持这种情况,您可以显式地使您的模块在每个进程中仅加载一次。例如
static int loaded = 0;
static int
exec_module(PyObject* module)
{
if (loaded) {
PyErr_SetString(PyExc_ImportError,
"cannot load module more than once per process");
return -1;
}
loaded = 1;
// ... rest of initialization
}
从函数访问模块状态¶
从模块级函数访问状态很简单。函数将模块对象作为它们的第一个参数;为了提取状态,您可以使用 PyModule_GetState
static PyObject *
func(PyObject *module, PyObject *args)
{
my_struct *state = (my_struct*)PyModule_GetState(module);
if (state == NULL) {
return NULL;
}
// ... rest of logic
}
注意
如果不存在模块状态,即 PyModuleDef.m_size
为零,PyModule_GetState
可能会返回 NULL
而不设置异常。在您自己的模块中,您可以控制 m_size
,因此很容易防止这种情况发生。
堆类型¶
传统上,在 C 代码中定义的类型是 *静态* 的;也就是说,static PyTypeObject
结构直接在代码中定义,并使用 PyType_Ready()
初始化。
这些类型必然在整个进程中共享。在模块对象之间共享它们需要注意它们拥有或访问的任何状态。为了限制可能出现的问题,静态类型在 Python 级别是不可变的:例如,您不能设置 str.myattribute = 123
。
CPython 实现细节: 在解释器之间共享真正不可变的对象是没问题的,只要它们不提供对可变对象的访问。然而,在 CPython 中,每个 Python 对象都有一个可变的实现细节:引用计数。对引用计数的更改由 GIL 保护。因此,在解释器之间共享任何 Python 对象的代码隐式地依赖于 CPython 当前的、进程范围的 GIL。
由于它们是不可变的且是进程全局的,静态类型无法访问“它们”的模块状态。如果这种类型的任何方法需要访问模块状态,则必须将该类型转换为*堆分配的类型*,或简称为 *堆类型*。这些类型更接近于 Python 的 class
语句创建的类。
对于新模块,默认使用堆类型是一个不错的经验法则。
将静态类型更改为堆类型¶
静态类型可以转换为堆类型,但请注意,堆类型 API 并非设计为从静态类型进行“无损”转换的——也就是说,创建一种与给定静态类型完全相同的类型。因此,在新的 API 中重写类定义时,您可能会无意中更改一些细节(例如,可 pickle 性或继承的槽)。始终测试对您重要的细节。
请特别注意以下两点(但请注意,这并非详尽列表)
与静态类型不同,默认情况下,堆类型对象是可变的。使用
Py_TPFLAGS_IMMUTABLETYPE
标志来防止可变性。堆类型默认继承
tp_new
,因此可以从 Python 代码实例化它们。您可以使用Py_TPFLAGS_DISALLOW_INSTANTIATION
标志来防止这种情况。
定义堆类型¶
可以通过填充 PyType_Spec
结构(类的描述或“蓝图”),并调用 PyType_FromModuleAndSpec()
来构造一个新的类对象,从而创建堆类型。
注意
其他函数(如 PyType_FromSpec()
)也可以创建堆类型,但 PyType_FromModuleAndSpec()
将模块与类关联起来,允许从方法访问模块状态。
类通常应该存储在模块状态(为了从 C 安全访问)和模块的 __dict__
中(为了从 Python 代码访问)。
垃圾回收协议¶
堆类型的实例保留对其类型的引用。这确保了类型不会在其所有实例之前被销毁,但可能会导致需要垃圾回收器打破的引用循环。
为了避免内存泄漏,堆类型的实例必须实现垃圾回收协议。也就是说,堆类型应该
具有
Py_TPFLAGS_HAVE_GC
标志。使用
Py_tp_traverse
定义一个遍历函数,该函数会访问类型(例如,使用Py_VISIT(Py_TYPE(self))
)。
有关其他考虑事项,请参阅 Py_TPFLAGS_HAVE_GC
和 tp_traverse
的文档。
定义堆类型的 API 有机地增长,使其在当前状态下使用起来有些笨拙。以下部分将指导您解决常见问题。
Python 3.8 及更低版本中的 tp_traverse
¶
从 tp_traverse
访问类型的要求是在 Python 3.9 中添加的。如果您支持 Python 3.8 及更低版本,则遍历函数必须*不*访问类型,因此它必须更复杂
static int my_traverse(PyObject *self, visitproc visit, void *arg)
{
if (Py_Version >= 0x03090000) {
Py_VISIT(Py_TYPE(self));
}
return 0;
}
不幸的是,Py_Version
仅在 Python 3.11 中添加。作为替代,请使用
PY_VERSION_HEX
,如果未使用稳定的 ABI,或者sys.version_info
(通过PySys_GetObject()
和PyArg_ParseTuple()
)。
委托 tp_traverse
¶
如果您的遍历函数委托给其基类的 tp_traverse
(或另一个类型),请确保 Py_TYPE(self)
仅被访问一次。请注意,只有堆类型才需要在 tp_traverse
中访问类型。
例如,如果您的遍历函数包括
base->tp_traverse(self, visit, arg)
…并且 base
可能是静态类型,那么它也应该包括
if (base->tp_flags & Py_TPFLAGS_HEAPTYPE) {
// a heap type's tp_traverse already visited Py_TYPE(self)
} else {
if (Py_Version >= 0x03090000) {
Py_VISIT(Py_TYPE(self));
}
}
定义 tp_dealloc
¶
如果您的类型具有自定义的 tp_dealloc
函数,则它需要
在任何字段失效之前调用
PyObject_GC_UnTrack()
,并且递减类型的引用计数。
为了在调用 tp_free
时保持类型有效,需要在实例被释放*之后*递减类型的引用计数。例如
static void my_dealloc(PyObject *self)
{
PyObject_GC_UnTrack(self);
...
PyTypeObject *type = Py_TYPE(self);
type->tp_free(self);
Py_DECREF(type);
}
默认的 tp_dealloc
函数会这样做,因此如果您的类型*不*覆盖 tp_dealloc
,则无需添加它。
不覆盖 tp_free
¶
堆类型的 tp_free
槽必须设置为 PyObject_GC_Del()
。这是默认值;请不要覆盖它。
避免 PyObject_New
¶
使用 GC 跟踪的对象需要使用支持 GC 的函数进行分配。
如果您使用 PyObject_New()
或 PyObject_NewVar()
获取并调用类型的
tp_alloc
槽(如果可能)。也就是说,将TYPE *o = PyObject_New(TYPE, typeobj)
替换为TYPE *o = typeobj->tp_alloc(typeobj, 0);
将
o = PyObject_NewVar(TYPE, typeobj, size)
替换为相同的代码,但使用 size 代替 0。如果上述操作不可行(例如,在自定义的
tp_alloc
中),请调用PyObject_GC_New()
或PyObject_GC_NewVar()
。TYPE *o = PyObject_GC_New(TYPE, typeobj); TYPE *o = PyObject_GC_NewVar(TYPE, typeobj, size);
从类访问模块状态¶
如果您使用 PyType_FromModuleAndSpec()
定义了一个类型对象,您可以调用 PyType_GetModule()
来获取关联的模块,然后调用 PyModule_GetState()
来获取模块的状态。
为了节省一些繁琐的错误处理样板代码,您可以将这两个步骤与 PyType_GetModuleState()
结合使用,结果如下:
my_struct *state = (my_struct*)PyType_GetModuleState(type);
if (state == NULL) {
return NULL;
}
从常规方法访问模块状态¶
从类的方法中访问模块级状态稍微复杂一些,但由于 Python 3.9 中引入的 API,这是可能的。要获取状态,您需要首先获取定义类,然后从中获取模块状态。
最大的障碍是获取方法在其中定义的类,或者简称该方法的“定义类”。定义类可以引用它所属的模块。
不要将定义类与 Py_TYPE(self)
混淆。如果在您的类型的子类上调用该方法,Py_TYPE(self)
将指向该子类,该子类可能在与您的模块不同的模块中定义。
注意
以下 Python 代码可以说明这个概念。Base.get_defining_class
返回 Base
,即使 type(self) == Sub
class Base:
def get_type_of_self(self):
return type(self)
def get_defining_class(self):
return __class__
class Sub(Base):
pass
为了让方法获取其“定义类”,它必须使用 METH_METHOD | METH_FASTCALL | METH_KEYWORDS 调用 约定
和相应的 PyCMethod
签名。
PyObject *PyCMethod(
PyObject *self, // object the method was called on
PyTypeObject *defining_class, // defining class
PyObject *const *args, // C array of arguments
Py_ssize_t nargs, // length of "args"
PyObject *kwnames) // NULL, or dict of keyword arguments
一旦有了定义类,调用 PyType_GetModuleState()
来获取其关联模块的状态。
例如
static PyObject *
example_method(PyObject *self,
PyTypeObject *defining_class,
PyObject *const *args,
Py_ssize_t nargs,
PyObject *kwnames)
{
my_struct *state = (my_struct*)PyType_GetModuleState(defining_class);
if (state == NULL) {
return NULL;
}
... // rest of logic
}
PyDoc_STRVAR(example_method_doc, "...");
static PyMethodDef my_methods[] = {
{"example_method",
(PyCFunction)(void(*)(void))example_method,
METH_METHOD|METH_FASTCALL|METH_KEYWORDS,
example_method_doc}
{NULL},
}
从槽方法、getter 和 setter 访问模块状态¶
注意
这是 Python 3.11 中的新特性。
槽方法——特殊方法的快速 C 等效项,例如 nb_add
用于 __add__
或 tp_new
用于初始化——有一个非常简单的 API,不允许像使用 PyCMethod
一样传入定义类。使用 PyGetSetDef
定义的 getter 和 setter 也是如此。
要在这些情况下访问模块状态,请使用 PyType_GetModuleByDef()
函数,并传入模块定义。一旦有了模块,调用 PyModule_GetState()
来获取状态。
PyObject *module = PyType_GetModuleByDef(Py_TYPE(self), &module_def);
my_struct *state = (my_struct*)PyModule_GetState(module);
if (state == NULL) {
return NULL;
}
PyType_GetModuleByDef()
通过搜索方法解析顺序(即所有超类)来查找第一个具有相应模块的超类。
注意
在非常特殊的情况下(继承链跨越从相同定义创建的多个模块),PyType_GetModuleByDef()
可能不会返回真正的定义类的模块。但是,它始终会返回一个具有相同定义的模块,从而确保兼容的 C 内存布局。
模块状态的生命周期¶
当模块对象被垃圾回收时,其模块状态将被释放。对于指向模块状态(的一部分)的每个指针,您必须保持对模块对象的引用。
通常这不是问题,因为使用 PyType_FromModuleAndSpec()
创建的类型及其实例都保持对模块的引用。但是,当您从其他地方(例如,外部库的回调)引用模块状态时,必须小心引用计数。
未解决的问题¶
围绕每个模块的状态和堆类型的几个问题仍然存在。
关于改进这种情况的讨论最好在 capi-sig 邮件列表上进行。
每个类的范围¶
目前(截至 Python 3.11),在不依赖 CPython 实现细节的情况下(这在未来可能会发生变化——也许,具有讽刺意味的是,允许针对每个类的范围的适当解决方案),无法将状态附加到单个类型。
无损转换为堆类型¶
堆类型 API 并非为从静态类型“无损”转换而设计;也就是说,创建一种与给定静态类型完全相同的类型。