转向限制性 Python API(PEP384)#

前言#

Python 支持限制性的 API,限制了对某些结构的访问。除了消除以下划线开头的完整模块、所有函数和宏外,最剧烈的限制是移除正常类型对象声明。

有关被消除的模块和函数的详细信息,请参阅PEP 384 页面。

变了模块#

所有更改模块的包含文件在此处列出。一般规则是尽量将更改减少到最小 diff。如果可能,将不可用的宏更改为具有相同名称的函数。完全移除的名称 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_STRINGPyBytes_GET_SIZE 已重新定义为调用相应函数。

floatobject.h#

PyFloat_AS_DOUBLE 现在调用 PyFloat_AsDouble

tupleobject.h#

PyTuple_GET_ITEMPyTuple_SET_ITEMPyTuple_GET_SIZE 已重新定义为函数调用。

listobject.h#

PyList_GET_ITEMPyList_SET_ITEMPyList_GET_SIZE 已重新定义为函数调用。

dictobject.h#

PyDict_GetItem 同样存在一个 PyDict_GetItemWithError 版本,该版本不会抑制错误。这种抑制会导致全局结构改变。此函数仅在 Python 2(自 Python 2.7.12)中存在,并具有不同的名称。我们只是实现了此函数。需要在访问字典时避免 GIL。

methodobject.h#

PyCFunction_GET_FUNCTIONPyCFunction_GET_SELFPyCFunction_GET_FLAGS 已重新定义为函数调用。

无法直接访问 methoddef 结构,我们定义了 PepCFunction_GET_NAMESTR 作为名称字符串的访问器。

pythonrun.h#

简单的函数 PyRun_String 不可用。它已在签名字模块中以简化版本重新实现。

funcobject.h#

虽然内部也有额外的 #ifdef 条件定义,但完全缺少 funcobject.h 的定义。这表明排除是不故意的。

因此,我们重新定义了 PyFunctionObject 为一个不透明类型。

缺少的宏 PyFunction_Check 已定义,宏 PyFunction_GET_CODE 调用相应的函数。

没有函数名访问的等效函数,因此我们引入了 PepFunction_GetName,它既可以作为函数也可以作为宏。

TODO:我们应修复 funcobject.h

classobject.h#

类对象也没有完全导入,而不是通过定义不透明类型。

我们定义了缺失的函数 PyMethod_NewPyMethod_FunctionPyMethod_Self,并且也将 PyMethod_GET_SELFPyMethod_GET_FUNCTION 重新定义为这些函数的调用。

TODO:我们应该修复 classobject.h 文件。

code.h<#>

整个 code.c 代码都消失了,尽管定义一些最小访问性可能是有意义的。这将在 Python-Dev 上进行说明。我们需要访问代码对象,并定义了缺失的 PepCode_GET_FLAGS 和 PepCode_GET_ARGCOUNT 或作为函数或宏。我们进一步添加了缺失的标志,尽管使用的并不多

CO_OPTIMIZED CO_NEWLOCALS CO_VARARGS CO_VARKEYWORDS CO_NESTED CO_GENERATOR

TODO:我们可能需要修复 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:我们可能需要提供对 datetime 的优化接口

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链接到本节

在将所有文件全部转换为对象.h 文件之前,我们有点震惊:我们突然意识到我们将再也无法访问类型对象,甚至更令人胆战心惊的是,我们使用的所有类型都必须是堆类型,仅此而已!

对于 PySide 这样的类型API,它在各种形式中大量使用堆类型扩展,这个问题看起来根本无法解决。最后,它得到了很好的解决,但正确设置该功能花费了将近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 函数会将空的 tp_dealloc 字段填充为 object_dealloc。现在,考虑到这一点而编写的代码可能会出现很大的错误,如果突然使用 subtype_dealloc

解决问题的方法是以明确的方式提供一个 object_dealloc 函数。这又会带来一个新的问题,因为 object_dealloc 不是公开的。自己编写版本并不困难,但同样需要访问类型对象。但幸运的是,我们已经打破了这项规则...

  • 全新类型只部分分配

PyType_FromSpec 中使用的结构几乎全部分配完成,只有名称字段是静态的。这对静态创建一次的类型来说没问题。但如果需要参数化并使用单个槽位和规范定义创建多个类型,用于 tp_name 的名称字段必须动态分配。这是误导性的,因为所有槽位已经都是副本。

  • 全新类型不支持特殊偏移量

特殊字段 tp_weaklistoffsettp_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函数中),而是创建了一个只有PySide需要的字段的限制性PyTypeObject

这是对受限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想要实现的关键:没有更多基于版本的字段。

简化#

在将所有类型扩展字段替换为宏调用后,我们可以移除以下依赖于版本的重新定义

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 然后使用类型描述中的所有槽定义并生成一个常规的堆类型对象。

未使用信息

我们对类型的许多事情都知道得很多,但它们本质上很清楚。

  1. 类型的基本结构始终相同,无论是静态类型还是堆类型。

  2. 类型的发展非常缓慢,一个字段永远不会被具有不同语义的另一个字段所取代。

固有规则(a)给我们以下信息:如果我们计算基本字段的偏移量,那么这些信息对于非堆类型也是可用的。

验证检查规则(b)是否仍然有效。

工作原理

验证的基本思想是使用 PyType_FromSpec 创建新类型,并查看在类型结构中这些字段出现在哪里。因此,我们构建了一个包含所有我们使用的字段的 PyType_Slot 结构,并确保这些值在类型中都是唯一的。

大多数字段都没被 PyType_FromSpec 询问,所以我们简单地使用了一些数值。一些字段被解释,如 tp_members。这个字段必须真的是 PyMemberDef。而且还有 tp_basetp_bases,它们必须是类型对象及其列表。最容易的是,我们不从头开始生成这些字段,而是从 type 对象 PyType_Type 中获取。

然后,人们会想到编写一个函数,在该无透明类型结构中搜索已知的值。

但我们能做得更好,并且可以乐观地使用观察(b):我们简单地使用有限的 PyTypeObject 结构,并假设每个字段正好落在我们期待的位置。

这就完成了整个证明:如果我们找到所有我们期望位置的不相交值,那么验证就完成了。

关于 tp_dict

关于 tp_dict 字段的一句话:在证明中,这个字段有一点特殊,因为它不出现在规范中,并且不能通过 type.__dict__ 容易地检查,因为那会创建一个 dictproxy 对象。那么我们如何证明它是正确的字典呢?

我们无论如何都要创建那个 PyMethodDef 结构,并且我们不把它留空,而是插入一个空函数。然后我们询问 tp_dict 字段它是否包含预期的对象,就这样!

#EOT