向有限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_STRINGPyBytes_GET_SIZE 被重新定义为调用相应的函数。

floatobject.h

PyFloat_AS_DOUBLE 现在调用 PyFloat_AsDouble

tupleobject.h

PyTuple_GET_ITEM, PyTuple_SET_ITEMPyTuple_GET_SIZE 被重新定义为函数调用。

listobject.h

PyList_GET_ITEM, PyList_SET_ITEMPyList_GET_SIZE 被重新定义为函数调用。

dictobject.h

PyDict_GetItem 也存在一个 PyDict_GetItemWithError 版本,该版本不会抑制错误。这种抑制有触及全局结构的副作用。该函数仅在 Python 2 中存在,自 Python 2.7.12 起,并且有一个不同的名称。我们简单地实现了该函数。在访问字典时需要避免 GIL。

methodobject.h

PyCFunction_GET_FUNCTION, PyCFunction_GET_SELFPyCFunction_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_FunctionPyMethod_Self,并且重新定义了 PyMethod_GET_SELFPyMethod_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_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中),而是创建了一个受限的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 然后使用类型描述中的所有槽定义并生成一个常规的堆类型对象。

未使用的信息

我们知道很多关于类型的事情,虽然没有明确说明, 但它们本质上是显而易见的:

  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