向有限Python API的过渡 (PEP384)¶
前言¶
Python 支持一个有限的 API,该 API 限制了对某些结构的访问。 除了消除整个模块以及所有名称以下划线开头的函数和宏之外, 最严格的限制是移除了正常的类型对象声明。
有关已移除模块和函数的详细信息,请参阅PEP 384页面以供参考。
已更改的模块¶
所有更改模块的包含文件及其更改的函数都在此列出。
一般来说,尽量保持更改的差异最小。
不可用的宏尽可能更改为同名的函数。
完全删除的名称 Py{name}
被重新实现为 Pep{name}
。
memoryobject.h¶
缓冲区协议已被完全移除。我们重新定义了所有的结构和方法,因为PySide使用了这些内容。这是我们有限API中的一个例外,我们必须自己进行检查。代码被提取到bufferprocs_py37.h文件中。这与以下内容相关:
abstract.h¶
这属于像memoryobject.h这样的缓冲区协议。
作为Py_buffer
的替代,我们定义了Pep_buffer
和其他几个内部宏。
版本是手动检查的,只有在实现没有变化时才必须更新版本号。否则,我们需要编写依赖于版本的代码路径。
继续使用缓冲区协议是否值得,或者我们是否应该尝试完全摆脱Pep_buffer
,这是值得怀疑的。
pydebug.h¶
我们无法直接访问Py_VerboseFlag
,因为不支持调试。我们将其重新定义为宏Py_VerboseFlag
,该宏调用Pep_VerboseFlag
。
unicodeobject.h¶
宏 PyUnicode_GET_SIZE
已被移除,并替换为 PepUnicode_GetLength
,
它在 Python 2 中评估为 PyUnicode_GetSize
,在 Python 3 中评估为 PyUnicode_GetLength
。
自 Python 3.3 起,PyUnicode_GetSize
会产生需要 GIL 的不良副作用!
函数 _PyUnicode_AsString
不可用,已被调用 _PepUnicode_AsString
的宏替换。实现有点复杂,最好更改代码并替换此函数。
bytesobject.h¶
宏 PyBytes_AS_STRING
和 PyBytes_GET_SIZE
被重新定义为调用相应的函数。
floatobject.h¶
PyFloat_AS_DOUBLE
现在调用 PyFloat_AsDouble
。
tupleobject.h¶
PyTuple_GET_ITEM
, PyTuple_SET_ITEM
和 PyTuple_GET_SIZE
被重新定义为函数调用。
listobject.h¶
PyList_GET_ITEM
, PyList_SET_ITEM
和 PyList_GET_SIZE
被重新定义为函数调用。
dictobject.h¶
PyDict_GetItem
也存在一个 PyDict_GetItemWithError
版本,该版本不会抑制错误。这种抑制有触及全局结构的副作用。该函数仅在 Python 2 中存在,自 Python 2.7.12 起,并且有一个不同的名称。我们简单地实现了该函数。在访问字典时需要避免 GIL。
methodobject.h¶
PyCFunction_GET_FUNCTION
, PyCFunction_GET_SELF
和 PyCFunction_GET_FLAGS
被重新定义为函数调用。
直接访问methoddef结构不可用,我们定义了PepCFunction_GET_NAMESTR
作为名称字符串的访问器。
pythonrun.h¶
简单的函数 PyRun_String
不可用。它被重新实现为一个简化版本用于签名模块。
funcobject.h¶
funcobject.h 的定义完全缺失,尽管里面还有额外的 #ifdef
条件定义。这表明排除是无意的。
因此,我们将PyFunctionObject
重新定义为不透明类型。
缺失的宏 PyFunction_Check
已被定义,宏
PyFunction_GET_CODE
调用了相应的函数。
没有等效的函数名称访问方式,因此我们引入了PepFunction_GetName
作为函数或宏。
待办事项:我们应该修复 funcobject.h
classobject.h¶
Classobject 也完全没有被导入,而是定义了一个不透明类型。
我们定义了缺失的函数 PyMethod_New
, PyMethod_Function
和
PyMethod_Self
,并且重新定义了 PyMethod_GET_SELF
和
PyMethod_GET_FUNCTION
为对这些函数的调用。
待办事项:我们应该修复 classobject.h
代码.h¶
整个code.c代码已经没有了,尽管定义一些最低限度的可访问性可能是有意义的。这将在Python-Dev上得到澄清。我们需要访问代码对象,并定义了缺失的PepCode_GET_FLAGS和PepCode_GET_ARGCOUNT,无论是作为函数还是宏。我们进一步添加了缺失的标志,尽管很少使用:
CO_OPTIMIZED
CO_NEWLOCALS
CO_VARARGS
CO_VARKEYWORDS
CO_NESTED
CO_GENERATOR
待办事项:我们或许应该修复 code.h
datetime.h¶
DateTime模块明确不包括在有限API中。 我们定义了所有需要的函数,但通过Python调用它们,而不是直接调用宏。这会对性能产生轻微影响。
通过提供一个一次性获取所有属性的接口,而不是每次都通过对象协议,可以轻松提高性能。
重新定义的宏和方法有:
PyDateTime_GET_YEAR
PyDateTime_GET_MONTH
PyDateTime_GET_DAY
PyDateTime_DATE_GET_HOUR
PyDateTime_DATE_GET_MINUTE
PyDateTime_DATE_GET_SECOND
PyDateTime_DATE_GET_MICROSECOND
PyDateTime_DATE_GET_FOLD
PyDateTime_TIME_GET_HOUR
PyDateTime_TIME_GET_MINUTE
PyDateTime_TIME_GET_SECOND
PyDateTime_TIME_GET_MICROSECOND
PyDateTime_TIME_GET_FOLD
PyDate_Check
PyDateTime_Check
PyTime_Check
PyDate_FromDate
PyDateTime_FromDateAndTime
PyTime_FromTime
XXX: 我们或许应该提供一个优化的接口来处理日期时间
object.h¶
文件 object.h 包含 PyTypeObject
结构体,该结构体应该是完全不透明的。所有对类型的访问都应该通过 PyType_GetSlot
调用来完成。由于有限 API 实现中的错误和缺陷,无法做到这一点。相反,我们为 PyTypeObject
定义了一个简化的结构体,该结构体仅包含 PySide 中使用的字段。
我们稍后会解释为什么以及如何做到这一点。以下是简化后的结构:
typedef struct _typeobject {
PyVarObject ob_base;
const char *tp_name;
Py_ssize_t tp_basicsize;
void *X03; // Py_ssize_t tp_itemsize;
void *X04; // destructor tp_dealloc;
void *X05; // printfunc tp_print;
void *X06; // getattrfunc tp_getattr;
void *X07; // setattrfunc tp_setattr;
void *X08; // PyAsyncMethods *tp_as_async;
void *X09; // reprfunc tp_repr;
void *X10; // PyNumberMethods *tp_as_number;
void *X11; // PySequenceMethods *tp_as_sequence;
void *X12; // PyMappingMethods *tp_as_mapping;
void *X13; // hashfunc tp_hash;
ternaryfunc tp_call;
reprfunc tp_str;
void *X16; // getattrofunc tp_getattro;
void *X17; // setattrofunc tp_setattro;
void *X18; // PyBufferProcs *tp_as_buffer;
void *X19; // unsigned long tp_flags;
void *X20; // const char *tp_doc;
traverseproc tp_traverse;
inquiry tp_clear;
void *X23; // richcmpfunc tp_richcompare;
Py_ssize_t tp_weaklistoffset;
void *X25; // getiterfunc tp_iter;
void *X26; // iternextfunc tp_iternext;
struct PyMethodDef *tp_methods;
void *X28; // struct PyMemberDef *tp_members;
void *X29; // struct PyGetSetDef *tp_getset;
struct _typeobject *tp_base;
PyObject *tp_dict;
descrgetfunc tp_descr_get;
void *X33; // descrsetfunc tp_descr_set;
Py_ssize_t tp_dictoffset;
initproc tp_init;
allocfunc tp_alloc;
newfunc tp_new;
freefunc tp_free;
inquiry tp_is_gc; /* For PyObject_IS_GC */
PyObject *tp_bases;
PyObject *tp_mro; /* method resolution order */
} PyTypeObject;
由于Python的一个问题,函数PyIndex_Check
不得不以一种不希望的方式定义。请参见文件pep384_issue33738.cpp。
有一些扩展结构已经被隔离为特殊的宏,这些宏动态计算扩展类型结构的正确偏移量:
PepType_SOTP
用于SbkObjectTypePrivate
PepType_SETP
用于SbkEnumTypePrivate
PepType_PFTP
用于PySideQFlagsTypePrivate
这些扩展结构的使用方法可以通过在源代码中搜索PepType_{four}
来最好地了解。
由于新的堆类型接口,某些类型的名称现在在tp_name
字段中包含模块名称。为了有一种兼容的方式来访问简单类型名称作为C字符串,已经编写了PepType_GetNameStr
,它会跳过带点的名称部分。
最后,函数 _PyObject_Dump
被排除在有限API之外。
这是一个我们一直希望可用的有用调试工具,
所以它再次被添加回来。无论如何,我们没有重新实现它,
因此不支持Windows。
因此,忘记调用此函数的调试调用将破坏COIN。 :-)
使用新的类型API¶
在转换了除object.h文件之外的所有内容后,我们有点震惊:突然之间,我们意识到将无法再访问类型对象,更可怕的是,我们使用的所有类型都必须是堆类型!
对于PySide来说,由于其在各种形式中大量使用堆类型扩展,情况看起来相当难以解决。最终,这个问题得到了很好的解决,但花费了将近3.5个月的时间才搞定。
在我们了解如何完成此操作之前,我们将解释API之间的差异及其后果。
接口¶
Python的旧类型API了解静态类型和堆类型。
静态类型被写成一个PyTypeObject
结构的声明,所有字段都已填充。例如,这里是Python类型object
的定义(Python 3.6):
PyTypeObject PyBaseObject_Type = {
PyVarObject_HEAD_INIT(&PyType_Type, 0)
"object", /* tp_name */
sizeof(PyObject), /* tp_basicsize */
0, /* tp_itemsize */
object_dealloc, /* tp_dealloc */
0, /* tp_print */
0, /* tp_getattr */
0, /* tp_setattr */
0, /* tp_reserved */
object_repr, /* tp_repr */
0, /* tp_as_number */
0, /* tp_as_sequence */
0, /* tp_as_mapping */
(hashfunc)_Py_HashPointer, /* tp_hash */
0, /* tp_call */
object_str, /* tp_str */
PyObject_GenericGetAttr, /* tp_getattro */
PyObject_GenericSetAttr, /* tp_setattro */
0, /* tp_as_buffer */
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE, /* tp_flags */
PyDoc_STR("object()\n--\n\nThe most base type"), /* tp_doc */
0, /* tp_traverse */
0, /* tp_clear */
object_richcompare, /* tp_richcompare */
0, /* tp_weaklistoffset */
0, /* tp_iter */
0, /* tp_iternext */
object_methods, /* tp_methods */
0, /* tp_members */
object_getsets, /* tp_getset */
0, /* tp_base */
0, /* tp_dict */
0, /* tp_descr_get */
0, /* tp_descr_set */
0, /* tp_dictoffset */
object_init, /* tp_init */
PyType_GenericAlloc, /* tp_alloc */
object_new, /* tp_new */
PyObject_Del, /* tp_free */
};
我们可以以PyType_Spec
结构的形式编写相同的结构,
甚至有一个不完整的工具abitype.py可以为我们进行这种转换。
经过一些修正后,结果如下所示:
static PyType_Slot PyBaseObject_Type_slots[] = {
{Py_tp_dealloc, (void *)object_dealloc},
{Py_tp_repr, (void *)object_repr},
{Py_tp_hash, (void *)_Py_HashPointer},
{Py_tp_str, (void *)object_str},
{Py_tp_getattro, (void *)PyObject_GenericGetAttr},
{Py_tp_setattro, (void *)PyObject_GenericSetAttr},
{Py_tp_richcompare, (void *)object_richcompare},
{Py_tp_methods, (void *)object_methods},
{Py_tp_getset, (void *)object_getsets},
{Py_tp_init, (void *)object_init},
{Py_tp_alloc, (void *)PyType_GenericAlloc},
{Py_tp_new, (void *)object_new},
{Py_tp_free, (void *)PyObject_Del},
{0, 0},
};
static PyType_Spec PyBaseObject_Type_spec = {
"object",
sizeof(PyObject),
0,
Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
PyBaseObject_Type_slots,
};
这个新结构几乎与旧结构兼容,但存在一些细微的差异。
新类型在一步中生成
这似乎没有问题,但由于PySide中类型的构建方式,情况非常复杂。类型是逐块组装的,最后调用了PyType_Ready
函数。
使用新的API,PyType_Ready
已经在PyType_FromSpec
的末尾被调用,这意味着类型创建的逻辑完全被颠倒了。
新类型始终是堆类型
使用新的类型创建函数,不再可能创建“普通”类型。相反,它们都必须分配在堆上并进行垃圾回收。用户通常不会注意到这一点。但类型创建受到更多限制,如果未设置Py_TPFLAGS_BASETYPE
,则无法创建子类型。PySide已经违反了这一限制,并需要一个相当深入的修复。
新类型总是需要一个模块
虽然这本身不是问题,但上述新的类型规范不会创建一个可用的新类型,而是会报错:
DeprecationWarning: builtin type object has no __module__ attribute
但还有更多问题:
新类型有出乎意料的默认值
当字段为空时,您通常会认为它们保持为空。
PyType_Ready
只会对类型进行一些修正。
但在PyType_FromSpec
中有以下条款可能会让你头疼不已:
if (type->tp_dealloc == NULL) {
/* It's a heap type, so needs the heap types' dealloc.
subtype_dealloc will call the base type's tp_dealloc, if
necessary. */
type->tp_dealloc = subtype_dealloc;
}
事实上,在迁移到新API之前,PyType_Ready
函数
用object_dealloc
填充了空的tp_dealloc
字段。而基于此编写的代码
如果突然使用subtype_dealloc
,现在就会变得非常错误。
解决方法是显式提供一个object_dealloc
函数。
这又会带来一个问题,因为object_dealloc
不是公开的。
编写我们自己的版本很容易,但它再次需要访问类型对象。但幸运的是,我们已经打破了这个规则……
新类型仅部分分配
在PyType_FromSpec
中使用的结构几乎都是动态分配的,只有名称字段是静态的。这对于静态创建一次的类型来说没有问题。但如果你想参数化事物并使用单个插槽和规范定义创建多个类型,那么用于tp_name的名称字段必须动态分配。这是误导性的,因为所有的插槽已经是副本。
新类型不支持特殊偏移量
特殊字段 tp_weaklistoffset
和 tp_dictoffset
不被 PyType_FromSpec
支持。遗憾的是,文档没有告诉你是否可以在创建类型后手动设置这些字段。我们最终这样做了并且成功了,但我们不确定其正确性。
请参见 basewrapper.cpp 函数 SbkObject_TypeF()
作为 PySide 中这些字段的唯一参考。这个唯一的参考是绝对必要且非常重要的,因为所有派生类型都隐式继承了这两个字段。
未来版本的有限API¶
正如我们所看到的,当前版本的有限API做了一些取巧,因为它使用了应该是透明类型的数据结构部分。目前,这种方法运行良好,因为数据的兼容性仍然比可能的要好。
但如果将来这种情况发生变化怎么办?
我们知道,在Python 3.8发布之前,数据结构是稳定的。在此之前,希望所有的小错误和遗漏都能得到解决。然后,就可以通过调用PyType_GetSlot
来替换当前的小技巧,按照应有的方式进行。
在当前关于数据结构的假设不再成立的那一刻,我们将用调用PyType_GetSlot
来重写直接属性访问。之后,将不再需要任何更改。
附录A:向更简单类型的过渡¶
在所有代码都转换为有限API后,PyHeapTypeObject
仍然存在一个问题。
为什么会有问题?因为shiboken中的所有类型结构都在堆类型对象的末尾使用了特殊的额外字段。这目前要求在编译时额外了解堆类型对象的大小。在一个干净的实现中,我们只会使用PyTypeObject
本身,并通过在运行时计算的指针访问类型后面的字段。
受限的 PyTypeObject¶
在我们深入细节之前,让我们先了解一下受限的PyTypeObject
的存在动机:
最初,我们想使用PyTypeObject
作为一个不透明类型,并限制自己只使用访问函数PyType_GetSlot
。这个函数允许访问有限API支持的所有字段。
但这是一种限制,因为我们无法访问tp_dict
,而我们需要它来支持签名扩展。但我们可以绕过这个问题。
真正的限制是PyType_GetSlot
仅适用于堆类型。这使得该函数相当无用,因为我们无法访问PyType_Type
,这是Python中最重要的类型type
。我们需要它来动态计算PyHeapTypeObject
的大小。
经过大量努力,可以将PyType_Type
克隆为堆类型。但由于Pep 384支持中的一个错误,我们需要访问普通类型的nb_index
字段。克隆没有帮助,因为PyNumberMethods
字段不被继承。
在我们意识到这个死胡同后,我们改变了概念,完全不再使用PyType_GetSlot
(除了在函数copyNumberMethods
中),而是创建了一个受限的PyTypeObject
,只定义了PySide中需要的那些字段。
这是否破坏了有限的API?我不这么认为。一个特殊的函数在程序启动时运行,检查PyTypeObject
字段的正确位置,尽管这些字段的变化几乎是不可能的。
真正关键的是不再显式使用PyHeapTypeObject
,因为它的布局确实会随着时间的推移而改变。
多样化¶
有多个Sbk{something}
结构体,它们都使用“d”字段来存储私有数据。这使得在对象和类型之间切换时很难找到正确的字段:
struct LIBSHIBOKEN_API SbkObject
{
PyObject_HEAD
PyObject *ob_dict;
PyObject *weakreflist;
SbkObjectPrivate *d;
};
struct LIBSHIBOKEN_API SbkObjectType
{
PyHeapTypeObject super;
SbkObjectTypePrivate *d;
};
第一步是将SbkObjectTypePrivate部分从“d”重命名为“sotp”。选择这个名称是为了简短但易于记忆,作为“SbkObjectTypePrivate”的缩写,结果如下:
struct LIBSHIBOKEN_API SbkObjectType
{
PyHeapTypeObject super;
SbkObjectTypePrivate *sotp;
};
重命名后,进行以下转换变得更加容易。
抽象¶
将类型扩展指针重命名为sotp
后,我将其替换为类似函数的宏,这些宏在类型背后进行特殊访问,而不是那些显式字段。例如,表达式:
type->sotp->converter
变成了:
PepType_SOTP(type)->converter
宏扩展可以在这里看到:
#define PepHeapType_SIZE \
(reinterpret_cast<PyTypeObject *>(&PyType_Type)->tp_basicsize)
#define _genericTypeExtender(etype) \
(reinterpret_cast<char *>(etype) + PepHeapType_SIZE)
#define PepType_SOTP(etype) \
(*reinterpret_cast<SbkObjectTypePrivate **>(_genericTypeExtender(etype)))
这看起来复杂,但最终只有一个通过PyType_Type
的新间接层,这是在运行时发生的。这是实现Pep 384想要达到的目标的关键:不再有版本依赖的字段。
简化¶
在所有类型扩展字段被宏调用替换后,我们可以移除以下版本依赖的PyHeapTypeObject
的重新定义
typedef struct _pyheaptypeobject {
union {
PyTypeObject ht_type;
void *opaque[PY_HEAPTYPE_SIZE];
};
} PyHeapTypeObject;
,以及版本依赖的结构:
struct LIBSHIBOKEN_API SbkObjectType
{
PyHeapTypeObject super;
SbkObjectTypePrivate *sotp;
};
可以被移除。SbkObjectType 仍然作为(已弃用的)类型别名存在,指向 PyTypeObject。
附录 B: PyTypeObject 的验证¶
我们在与原始PyTypeObject相同的位置引入了一个有限的PyTypeObject,现在我们需要证明我们被允许这样做。
当按照预期使用有限API时,类型是完全不透明的,只能通过PyType_FromSpec
和(从版本3.5开始)通过PyType_GetSlot
进行访问。
Python 然后使用类型描述中的所有槽定义并生成一个常规的堆类型对象。
未使用的信息¶
我们知道很多关于类型的事情,虽然没有明确说明, 但它们本质上是显而易见的:
类型的基本结构始终相同,无论它是静态类型还是堆类型。
类型演变非常缓慢,一个字段永远不会被具有不同语义的另一个字段替换。
固有规则 (a) 为我们提供了以下信息:如果我们计算基本字段的偏移量,那么这些信息也适用于非堆类型。
验证检查规则 (b) 是否仍然有效。
它是如何工作的¶
验证的基本思想是使用
PyType_FromSpec
生成一个新类型,并查看这些字段在类型结构中的位置。因此,我们构建了一个PyType_Slot
结构,其中包含我们使用的所有字段,并确保这些值在类型中都是唯一的。
大多数字段不会被PyType_FromSpec
询问,因此我们简单地使用了一些数值。有些字段被解释,比如tp_members
。这个字段必须是一个PyMemberDef
。还有tp_base
和tp_bases
,它们必须是类型对象及其列表。最简单的方法不是从头开始生成这些字段,而是从type
对象PyType_Type
中使用它们。
然后人们会想到编写一个函数来搜索不透明类型结构中的已知值。
但我们可以做得更好,并乐观地利用观察(b):
我们只需使用受限的PyTypeObject
结构,并假设
每个字段都准确地落在我们期望的位置。
这就是整个证明:如果我们在预期的地方找到所有不相交的值,那么验证就完成了。
关于 tp_dict
¶
关于tp_dict
字段的一句话:这个字段在证明中有点特殊,因为它没有出现在规范中,并且不容易通过type.__dict__
检查,因为这会创建一个dictproxy对象。那么我们如何证明这确实是正确的字典呢?
我们必须创建那个PyMethodDef
结构体,而不是让它空着,我们插入一个虚拟函数。然后我们询问tp_dict
字段是否包含我们期待的对象,就这样!
#EOT