Following system colour scheme - Python 增强提案 Selected dark colour scheme - Python 增强提案 Selected light colour scheme - Python 增强提案

Python 增强提案

PEP 793 – PyModExport:C 扩展模块的新入口点

作者:
Petr Viktorin <encukou at gmail.com>
讨论至:
Discourse 帖子
状态:
草案
类型:
标准跟踪
创建日期:
2025年5月23日
Python 版本:
3.15
发布历史:
2025年3月14日, 2025年5月27日

目录

摘要

在此 PEP 中,我们为 C 扩展模块提出了一个新的入口点,通过它可以使用 PyModuleDef_Slot 结构数组来定义模块,而无需包含 PyModuleDef 结构。这允许扩展作者避免使用静态分配的 PyObject,从而消除了使一个编译后的库文件可用于 CPython 的常规构建和自由线程构建的最常见障碍。

为了使其可行,我们还指定了新的模块槽类型来替换 PyModuleDef 的字段,并允许添加一个类似于用于类型对象的 Py_tp_token 的*令牌*。

我们还添加了一个 API,用于从槽动态定义模块。

现有 API (PyInit_*) 被软弃用。(也就是说:它将继续工作而没有警告,并且将得到充分文档和支持,但我们计划不再向其添加任何新功能。)

背景与动机

Python 对象的内存布局在常规构建和自由线程构建之间有所不同。因此,支持常规构建和自由线程构建的 ABI 不能包含当前的 PyObject 内存布局。为了与现有 ABI(和 API)保持兼容,它不能支持静态分配的 Python 对象。

有一种对象在大多数扩展模块中都需要,并且在几乎所有情况下都是静态分配的:从模块导出钩子(即 PyInit_* 函数)返回的 PyModuleDef

模块导出钩子(PyInit_* 函数)可以返回两种对象:

  1. 一个完全初始化的模块对象(用于所谓的*单阶段初始化*)。这是 3.4 及以下版本中唯一的选项。以这种方式创建的模块在多个解释器或重复加载时具有令人惊讶(但向后兼容)的行为。(具体来说,此类模块的 __dict__ 的*内容*在模块对象的所有实例之间共享。)

    返回的模块通常使用 PyModule_Create 函数创建,该函数需要一个静态分配(或至少是长生命周期)的 PyModuleDef 结构体。

    可以通过使用低级 PyModule_New* API 来绕过此限制。这避免了对 PyModuleDef 的需求,但功能要少得多。

  2. 一个包含如何创建模块对象的描述的 PyModuleDef 对象。这个选项,即*多阶段初始化*,是在 PEP 489 中引入的;请参阅其动机以了解其存在的原因。

解释器在调用导出钩子之前无法区分这些情况。

解释器切换

Python 3.12 为模块添加了一种标记它们是否可以在子解释器中加载的方式:Py_mod_multiple_interpreters 槽。将其设置为“不支持”值表示扩展只能在主解释器中加载。

不幸的是,Python 只能通过*调用*模块导出钩子来获取此信息。对于单阶段模块,这会创建模块对象并运行任意初始化代码。对于将 Py_mod_multiple_interpreters 设置为“不支持”的模块,此初始化需要在主解释器中进行。

为了使其工作,如果在新模块加载到子解释器中时,Python 会暂时切换到主解释器,在那里调用导出钩子,然后要么切换回来并重新导入,要么失败。

这种不必要且脆弱的额外工作凸显了底层设计问题:Python 无法在扩展可能完全初始化自身之前获取有关扩展的信息。

基本原理

为了避免模块导出钩子需要静态分配的 PyObject*,有两个选项浮现:

  • 返回一个*动态*分配的对象,其所有权转移给解释器。这个结构可以与现有的 PyModuleDef 非常相似,因为它需要包含相同的数据。与现有的 PyModuleDef 不同,这个需要进行引用计数,以便它既比“其”模块的生命周期更长,又不会泄漏。
  • 添加一个不返回 PyObject* 的新导出钩子。

    这在 PEP 489 中已为 Python 3.5 考虑过,但被拒绝了:

    保留 PyInit 钩子名称,即使它不完全适合导出定义,也产生了一个更简单的解决方案。

    唉,在解决了这个选择所带来的十年影响之后,解决方案不再简单。

一个新的钩子还将允许 Python 避免动机中提到的第二个问题——解释器切换。实际上,它将为多阶段初始化添加一个新阶段,在此阶段 Python 可以检查模块是否兼容。

不使用包装结构体而使用槽

现有的 PyModuleDef 是一个具有一些固定字段和“槽”数组的结构。与槽不同,固定字段不能单独弃用和替换。本提案废除了固定字段,并建议直接使用槽数组,而无需包装结构体。

PyModuleDef_Slot 结构体与固定字段相比确实存在一些缺点。我们认为这些问题是可以解决的,但将其排除在本 PEP 的范围之外(参见“可能未来的方向”部分中的“全面改进槽”)。

令牌

静态 PyModuleDef 除了描述如何创建模块之外,还有另一个目的。作为一个静态分配的单例,它附加到模块对象,允许扩展作者检查给定的 Python 模块是否“属于他们”:如果一个模块对象有一个已知的 PyModuleDef,其模块状态将具有已知的内存布局。

通过添加 Py_tp_token 解决了类型对象的类似问题。本提案为模块添加了相同的机制。

与类型不同,导入机制通常有一个已知适合作为令牌值的指针;在这些情况下,它可以提供默认令牌。因此,模块令牌不需要 Py_TP_USE_SPEC 的不雅变体。

为了帮助跨 Python 版本的扩展,PyModuleDef 地址被用作默认令牌,并且在合理的情况下,它们可以与令牌互换。

软弃用现有导出钩子

现有扩展的作者切换到这里提出的 API 的唯一原因是,它允许一个模块用于自由线程和非自由线程构建。Python 允许这样做很重要,但对于许多现有模块来说,这绝不值得失去与 3.14 及更低版本的兼容性。

现在计划弃用旧 API 还为时过早。

相反,本 PEP 提议停止向 PyInit_* 方案添加新功能。毕竟,扩展作者切换的最佳时机是他们无论如何都想修改模块初始化的时候。

规范

导出钩子

导入扩展模块时,Python 现在将首先查找如下所示的导出钩子:

PyModuleDef_Slot *PyModExport_<NAME>(void);

其中 <NAME> 是模块的名称。对于非 ASCII 名称,它将改为查找 PyModExportU_<NAME>,其中 <NAME> 编码方式与现有 PyInitU_* 钩子相同(即,使用连字符替换为下划线的 *punycode* 编码)。

如果未找到,导入将像以前的 Python 版本一样继续(即,通过查找 PyInit_*PyInitU_* 函数)。

如果找到,Python 将不带任何参数调用钩子。

失败时,导出钩子必须返回 NULL 并设置异常。这将导致导入失败。(Python 在出错时不会回退到 PyInit_*。)

成功时,钩子必须返回一个指向 PyModuleDef_Slot 结构体数组的指针。然后 Python 将通过调用下面提出的函数 PyModule_FromSlotsAndSpecPyModule_Exec,根据给定槽创建模块。有关槽数组的要求,请参见其描述。

返回的数组及其指向的所有数据(递归地)必须保持有效和常量,直到运行时关闭。(我们期望函数导出静态常量,或者根据例如 Py_Version 选择的几个常量之一。动态行为通常应发生在 Py_mod_createPy_mod_exec 函数中。)

动态创建

将添加一个新函数,用于从槽数组创建模块:

PyObject *PyModule_FromSlotsAndSpec(const PyModuleDef_Slot *slots, PyObject *spec)

参数 *slots* 必须指向一个以 slot=0(通常在 C 中写作 {0})的槽终止的 PyModuleDef_Slot 结构数组。没有必需的槽,尽管 *slots* 不能是 NULL。因此,最小输入只包含终止槽。

注意

如果 PEP 803 被接受,Py_mod_abi 槽将是强制性的。

参数 *spec* 是一个鸭子类型(duck-typed)的 ModuleSpec 类对象,这意味着为 importlib.machinery.ModuleSpec 定义的任何属性都具有匹配的语义。name 属性是必需的,但此限制将来可能会解除。name 将*代替* Py_mod_name 槽使用(就像 PyModule_FromDefAndSpec 忽略 PyModuleDef.m_name 一样)。

PyModule_FromSlotsAndSpec 和新导出钩子的槽数组都只允许最多一个 Py_mod_exec 槽。PyModuleDef.m_slots 中的数组可能包含更多;这不会改变。此限制易于规避,并且很少使用多个 exec[1]

对于没有 PyModuleDef 创建的模块,Py_mod_create 函数将以 NULL 作为第二个参数(*def*)调用。(将来,如果我们发现传递输入槽数组的用例,可以添加一个具有更新签名的全新槽。)

PyModExport_* 钩子不同,*slots* 数组可以在 PyModule_FromSlotsAndSpec 调用后更改或销毁。(也就是说,Python 必须复制所有输入数据。)作为例外,由 Py_mod_methods 给出的任何 PyMethodDef 数组必须是静态分配的(或以其他方式保证其生命周期长于从中创建的对象)。此限制将来可能会解除。

将添加一个新函数 PyModule_Exec,用于运行模块的 exec 槽。其作用类似于 PyModule_ExecDef,但支持使用槽创建的模块,并且不接受显式 *def* 参数:

int PyModule_Exec(PyObject *module)

调用此函数是完全初始化模块所必需的。PyModule_FromSlotsAndSpec 将*不会*运行它(就像 PyModule_FromDefAndSpec 不调用 PyModule_ExecDef 一样)。

对于从 *def* 创建的模块,调用此函数等同于调用 PyModule_ExecDef(module, PyModule_GetDef(module))

令牌

模块对象将可选地存储一个“令牌”:一个类似于类型对象的 Py_tp_tokenvoid* 指针。

注意

这是一个专门的功能,旨在替换 PyType_GetModuleByDef 函数;不需要 PyType_GetModuleByDef 的用户很可能也不需要令牌。

本节包含技术规范;有关预期用法的示例,请参阅示例部分中的exampletype_repr

如果通过新的 Py_mod_token 槽指定,则模块令牌必须:

  • 比模块寿命长,因此在模块存在时不会被重复用于其他用途;并且
  • “属于”模块所在的扩展模块,因此不会与其他扩展模块冲突。

(通常,它应该是创建模块的槽数组或 PyModuleDef,或者是用于动态创建模块的另一个静态常量。)

PyModuleDef 的地址用作模块的令牌时,模块的行为应如同从该 PyModuleDef 创建一样。特别是,模块状态必须具有匹配的布局和语义。

使用 PyModule_FromSlotsAndSpecPyModExport_<NAME> 导出钩子创建的模块可以使用新的 Py_mod_token 槽来设置令牌。

PyModuleDef 创建的模块将把令牌设置为该定义。对于这些模块,显式的 Py_mod_token 槽将被拒绝。(这允许实现共享令牌和定义的存储。)

对于通过新导出钩子创建的模块,令牌将默认为槽数组的地址。(这*不*适用于通过 PyModule_FromSlotsAndSpec 创建的模块,因为该函数的输入可能不会比模块寿命更长。)

对于非 PyModuleType 实例,令牌将不设置。

将添加一个 PyModule_GetToken 函数来获取令牌。由于结果可能为 NULL,它将通过指针传递;函数成功时返回 0,失败时返回 -1。

int PyModule_GetToken(PyObject *, void **token_p)

将添加一个新函数 PyType_GetModuleByToken,其签名类似于现有的 PyType_GetModuleByDef,但接受一个 const void *token 参数,行为相同,只是匹配令牌而不是仅匹配定义,并返回一个强引用。

为了更容易向后兼容,现有的 PyType_GetModuleByDef 将更改为也允许令牌(转换为 PyModuleDef * 指针)作为 *def* 参数。也就是说,PyType_GetModuleByTokenPyType_GetModuleByDef 仅在第二个参数的正式签名和返回借用引用或强引用方面有所不同。(PyModule_GetDef 函数将不会获得类似的更改,因为用户可能会访问其结果的成员。)

新槽

对于 PyModuleDef 结构体的每个字段,除了 PyModuleDef_HEAD_INIT 中的字段外,都将提供一个新的槽 ID:Py_mod_namePy_mod_docPy_mod_clear 等。与模块状态而非模块对象相关的槽将使用 Py_mod_state_ 前缀。有关完整列表,请参阅新 API 摘要

所有新槽——包括上面讨论的 Py_tp_token——不能在槽数组中重复,也不能在 PyModuleDef.m_slots 数组中使用。它们不能具有 NULL 值(相反,该槽可以完全省略)。

请注意,目前,对于从 *spec* 创建的模块(即使用 PyModule_FromDefAndSpec),PyModuleDef.m_name 成员会被忽略,而是使用来自 *spec* 的名称。本文档中提出的所有 API 都是从 *spec* 创建模块,并且它将以同样的方式忽略 Py_mod_name。该槽将是可选的,但强烈鼓励扩展作者包含它,以利于未来的 API、外部工具、调试和内省。

零碎部分

将添加一个 PyMODEXPORT_FUNC 宏,类似于 PyMODINIT_FUNC 宏,但返回类型为 PyModuleDef_Slot *

将添加一个 PyModule_GetStateSize 函数,用于检索由 Py_mod_state_sizePyModuleDef.m_size 设置的大小。由于结果可能为 -1(对于单阶段初始化模块),它将通过指针输出;函数成功时返回 0,失败时返回 -1。

int PyModule_GetStateSize(PyObject *, Py_ssize_t *result);

软弃用现有导出钩子

PyInit_* 导出钩子将是软弃用的。

新 API 摘要

Python 将加载一个新的模块导出钩子,有两种变体:

PyModuleDef_Slot *PyModExport_<NAME>(void);
PyModuleDef_Slot *PyModExportU_<ENCODED_NAME>(void);

将添加以下函数:

PyObject *PyModule_FromSlotsAndSpec(const PyModuleDef_Slot *, PyObject *spec)
int PyModule_Exec(PyObject *)
int PyModule_GetToken(PyObject *, void**)
PyObject *PyType_GetModuleByToken(PyTypeObject *type, const void *token)
int PyModule_GetStateSize(PyObject *, Py_ssize_t *result);

将添加一个新宏:

PyMODEXPORT_FUNC

以及新的槽类型(小型整数的 #define 名称):

  • Py_mod_name (等同于 PyModuleDef.m_name)
  • Py_mod_doc (等同于 PyModuleDef.m_doc)
  • Py_mod_state_size (等同于 PyModuleDef.m_size)
  • Py_mod_methods (等同于 PyModuleDef.m_methods)
  • Py_mod_state_traverse (等同于 PyModuleDef.m_traverse)
  • Py_mod_state_clear (等同于 PyModuleDef.m_clear)
  • Py_mod_state_free (等同于 PyModuleDef.m_free)
  • Py_mod_token (见上文)

所有这些都将添加到受限 API 中。

向后兼容性

如果一个现有模块被移植到使用新机制,那么 PyModule_GetDef 将开始为它返回 NULL。(这与 PyModule_GetDef 的当前文档相符。)我们认为模块的定义方式是该模块的实现细节,因此这不应被视为破坏性更改。

类似地,PyType_GetModuleByDef 函数可能停止匹配其定义已更改的模块。模块作者可以通过将 *def* 显式设置为 *token* 来避免此问题。

PyType_GetModuleByDef 现在将接受一个模块令牌作为 *def* 参数。我们对使用 PyModuleDef 地址作为令牌指定了适当的限制,并且非 PyModuleDef 指针以前是无效输入,因此这不是一个向后兼容性问题。

Py_mod_create 函数现在可以以 NULL 作为第二个参数调用。这可能会让从 *def* 移植到 *slots* 的人感到困惑,因此需要在移植说明中提及。

向前兼容性

如果模块定义了新的导出钩子,那么实现此 PEP 的 CPython 版本将忽略传统的 PyInit_* 钩子。

跨 Python 版本的扩展预计会同时定义这两个钩子;CPython 的每个构建都会“选择”它支持的最新版本。

移植指南

这是一个将现有模块转换为新 API 的指南,包括一些棘手的边缘情况。它应该作为 HOWTO 移至文档中。

本指南适用于手动编写的模块。对于代码生成器和语言包装器,下面的向后兼容性垫片可能更有用。

  1. 扫描您的代码中 PyModule_GetDef 的用法。此函数将为使用新机制的模块返回 NULL。相反,
    • 要获取模块 PyModuleDef 的内容,请直接使用 C 结构体。或者,通过例如 PyModule_GetNameObject__doc__ 属性和 PyModule_GetStateSize 从模块中获取属性。(请注意,Python 代码可以修改模块的属性。)
    • 要测试模块对象是否“属于你”,请改用 PyModule_GetToken。在本指南的后面,你将把令牌设置为*就是*现有的 PyModuleDef 结构。
  2. (可选)扫描代码中 PyType_GetModuleByDef 的用法,并将其替换为 PyType_GetModuleByToken。在本指南的后面,您将把令牌设置为*就是*现有的 PyModuleDef 结构。

    (如果目标 Python 版本不支持 PyType_GetModuleByToken,则可以跳过此步骤,因为 PyType_GetModuleByDef 向后兼容。)

  3. 查看 Py_mod_create 标识的函数(如果有)。确保它不使用其第二个参数(PyModuleDef),因为它将以 NULL 调用。 instead of the argument, use the existing PyModuleDef struct directly。
  4. 如果使用多个 Py_mod_exec 槽,请将它们合并:选择其中一个函数,或编写一个新函数,并从它调用其他函数。删除除了一个 Py_mod_exec 槽之外的所有槽。
  5. 复制您的 PyModuleDefm_slots 成员指向的现有 PyModuleDef_Slot 数组。如果您没有现有槽数组,请像这样创建一个:
    static PyModuleDef_Slot module_slots[] = {
        {0}
    };
    

    给这个数组一个唯一的名称。后续示例将假定您将其命名为 module_slots

  6. 为现有 PyModuleDef 结构体的所有成员添加槽。有关新槽的列表,请参阅新 API 摘要。例如,要添加名称和文档字符串:
    static PyModuleDef_Slot module_slots[] = {
        {Py_mod_name, "mymodule"},
        {Py_mod_doc, (char*)PyDoc_STR("my docstring")},
        // ... (keep existing slots here)
        {0}
    };
    
  7. 如果您从 PyModule_GetDef 切换到 PyModule_GetToken,和/或如果您使用 PyType_GetModuleByDefPyType_GetModuleByToken,请添加一个指向现有 PyModuleDef 结构体的 Py_mod_token 槽:
    static PyModuleDef_Slot module_slots[] = {
        // ... (keep existing slots here)
        {Py_mod_token, &your_module_def},
        {0}
    };
    
  8. 添加一个新的导出钩子。
    PyMODEXPORT_FUNC PyModExport_examplemodule(PyObject);
    
    PyMODEXPORT_FUNC
    PyModExport_examplemodule(void)
    {
        return module_slots;
    }
    

新的导出钩子将在 Python 3.15 及更高版本上使用。一旦您的模块不再支持较低版本:

  1. 删除 PyInit_ 函数。
  2. 如果现有 PyModuleDef 结构体*仅*用于 Py_mod_token 和/或 PyType_GetModuleByToken,您可以删除 Py_mod_token 行,并将所有其他地方的 &your_module_def 替换为 module_slots
  3. 删除任何未使用的数据。PyModuleDef 结构体和原始槽数组很可能未被使用。

向后兼容垫片

可以编写一个通用函数,以本文提出的 API 的方式实现“旧”导出钩子(PyInit_)。

以下实现可以复制并粘贴到项目中;只需调整 PyInit_examplemodule(两次)和 PyModExport_examplemodule 的名称。

当将其添加到下面的示例并使用本 PEP 参考实现的非自由线程构建进行编译时,生成的扩展将与非自由线程 3.9+ 构建兼容,此外还与参考实现的自由线程构建兼容。(模块必须不带版本标签命名,例如 examplemodule.so,并放置在 sys.path 上。)

要完全支持创建此类模块,需要回溯一些新的 API,并支持构建/安装工具。这超出了本 PEP 的范围。(特别是,演示通过使用有限 API 3.15 的一个子集来“作弊”,该子集*恰好适用于* 3.9;一个合适的实现将使用有限 API 3.9,并为 Py_mod_name 等新 API 提供回溯垫片。)

此实现对槽数组提出了一些额外的要求:

  • 对应于 PyModuleDef 成员的槽必须首先出现。
  • 需要 Py_mod_name 槽。
  • 任何 Py_mod_token 都必须设置为此处定义的 &module_def_and_token
#include <string.h>     // memset

PyMODINIT_FUNC PyInit_examplemodule(void);

static PyModuleDef module_def_and_token;

PyMODINIT_FUNC
PyInit_examplemodule(void)
{
    PyModuleDef_Slot *slot = PyModExport_examplemodule();

    if (module_def_and_token.m_name) {
        // Take care to only set up the static PyModuleDef once.
        // (PyModExport might theoretically return different data each time.)
        return PyModuleDef_Init(&module_def_and_token);
    }
    int copying_slots = 1;
    for (/* slot set above */; slot->slot; slot++) {
        switch (slot->slot) {
        // Set PyModuleDef members from slots. These slots must come first.
#       define COPYSLOT_CASE(SLOT, MEMBER, TYPE)                            \
            case SLOT:                                                      \
                if (!copying_slots) {                                       \
                    PyErr_SetString(PyExc_SystemError,                      \
                                    #SLOT " must be specified earlier");    \
                    goto error;                                             \
                }                                                           \
                module_def_and_token.MEMBER = (TYPE)(slot->value);          \
                break;                                                      \
            /////////////////////////////////////////////////////////////////
        COPYSLOT_CASE(Py_mod_name, m_name, char*)
        COPYSLOT_CASE(Py_mod_doc, m_doc, char*)
        COPYSLOT_CASE(Py_mod_state_size, m_size, Py_ssize_t)
        COPYSLOT_CASE(Py_mod_methods, m_methods, PyMethodDef*)
        COPYSLOT_CASE(Py_mod_state_traverse, m_traverse, traverseproc)
        COPYSLOT_CASE(Py_mod_state_clear, m_clear, inquiry)
        COPYSLOT_CASE(Py_mod_state_free, m_free, freefunc)
        case Py_mod_token:
            // With PyInit_, the PyModuleDef is used as the token.
            if (slot->value != &module_def_and_token) {
                PyErr_SetString(PyExc_SystemError,
                                "Py_mod_token must be set to "
                                "&module_def_and_token");
                goto error;
            }
            break;
        default:
            // The remaining slots become m_slots in the def.
            // (`slot` now points to the "rest" of the original
            //  zero-terminated array.)
            if (copying_slots) {
                module_def_and_token.m_slots = slot;
            }
            copying_slots = 0;
            break;
        }
    }
    if (!module_def_and_token.m_name) {
        // This function needs m_name as the "is initialized" marker.
        PyErr_SetString(PyExc_SystemError, "Py_mod_name slot is required");
        goto error;
    }
    return PyModuleDef_Init(&module_def_and_token);

error:
    memset(&module_def_and_token, 0, sizeof(module_def_and_token));
    return NULL;
}

安全隐患

暂无

如何教授此内容

除了常规参考文档,移植指南应作为新的 HOWTO 添加。

示例

/*
Example module with C-level module-global state, and

- a simple function that updates and queries the state
- a class wihose repr() queries the same module state (as an example of
  PyType_GetModuleByToken)

Once compiled and renamed to not include a version tag (for example
examplemodule.so on Linux), this will run succesfully on both regular
and free-threaded builds.

Python usage:

import examplemodule
print(examplemodule.increment_value())  # 0
print(examplemodule.increment_value())  # 1
print(examplemodule.increment_value())  # 2
print(examplemodule.increment_value())  # 3


class Subclass(examplemodule.ExampleType):
    pass

instance = Subclass()
print(instance)  # <Subclass object; module value = 3>

*/

// Avoid CPython-version-specific ABI (inline functions & macros):
#define Py_LIMITED_API 0x030f0000  // 3.15

#include <Python.h>

typedef struct {
    int value;
} examplemodule_state;

static PyModuleDef_Slot examplemodule_slots[];

// increment_value function

static PyObject *
increment_value(PyObject *module, PyObject *_ignored)
{
    examplemodule_state *state = PyModule_GetState(module);
    int result = ++(state->value);
    return PyLong_FromLong(result);
}

static PyMethodDef examplemodule_methods[] = {
    {"increment_value", increment_value, METH_NOARGS},
    {NULL}
};

// ExampleType

static PyObject *
exampletype_repr(PyObject *self)
{
    /* To get module state, we cannot use PyModule_GetState(Py_TYPE(self)),
     * since Py_TYPE(self) might be a subclass defined in an unrelated module.
     * So, use PyType_GetModuleByToken.
     */
    PyObject *module = PyType_GetModuleByToken(
        Py_TYPE(self), examplemodule_slots);
    if (!module) {
        return NULL;
    }
    examplemodule_state *state = PyModule_GetState(module);
    Py_DECREF(module);
    if (!state) {
        return NULL;
    }
    return PyUnicode_FromFormat("<%T object; module value = %d>",
                                self, state->value);
}

static PyType_Spec exampletype_spec = {
    .name = "examplemodule.ExampleType",
    .flags = Py_TPFLAGS_DEFAULT | Py_TPFLAGS_BASETYPE,
    .slots = (PyType_Slot[]) {
        {Py_tp_repr, exampletype_repr},
        {0},
    },
};

// Module

static int
examplemodule_exec(PyObject *module) {
    examplemodule_state *state = PyModule_GetState(module);
    state->value = -1;
    PyTypeObject *type = (PyTypeObject*)PyType_FromModuleAndSpec(
        module, &exampletype_spec, NULL);
    if (!type) {
        return -1;
    }
    if (PyModule_AddType(module, type) < 0) {
        Py_DECREF(type);
        return -1;
    }
    Py_DECREF(type);
    return 0;
}

PyDoc_STRVAR(examplemodule_doc, "Example extension.");

static PyModuleDef_Slot examplemodule_slots[] = {
    {Py_mod_name, "examplemodule"},
    {Py_mod_doc, (char*)examplemodule_doc},
    {Py_mod_methods, examplemodule_methods},
    {Py_mod_state_size, (void*)sizeof(examplemodule_state)},
    {Py_mod_exec, (void*)examplemodule_exec},
    {0}
};

// Avoid "implicit declaration of function" warning:
PyMODEXPORT_FUNC PyModExport_examplemodule(void);

PyMODEXPORT_FUNC
PyModExport_examplemodule(void)
{
    return examplemodule_slots;
}

参考实现

GitHub 分支中提供了实现草案。

未解决的问题

(添加你的!)

被拒绝的想法

导出数据指针而非函数

这提出了一种新的模块导出*函数*,它预计会返回静态常量数据。这些数据可以直接作为数据指针导出。

使用函数,我们避免处理一种新的导出符号。

函数还允许扩展以有限的方式内省其环境——例如,根据当前的 Python 版本定制返回的数据。

PyModuleDef 更改为不是 PyObject

可以将 PyModuleDef 更改为不再包含 PyObject 头文件,并继续使用当前的 PyInit_* 钩子。这种方法存在几个问题:

  • 导入机制需要检查对象中的位模式,以区分不同的内存布局:
    • “旧的”基于 PyObjectPyModuleDef,由当前 abi3 扩展返回,
    • 新的 PyModuleDef
    • 基于 PyObject 的模块对象,用于单阶段初始化。

    这是脆弱的,并对 PyObject 未来的更改施加了限制:内存布局需要保持*可区分*,直到单阶段初始化和当前稳定 ABI 都不再受支持。

  • PyModuleDef_Init 的文档说明其作用是“确保模块定义是一个正确初始化的 Python 对象,正确报告其类型和引用计数。”这需要不经警告地更改,从而破坏任何将 PyModuleDef 视为 Python 对象的用户代码。

可能的未来方向

这些想法超出了*本*提案的范围。

全面改进槽

槽——特别是现有的 PyModuleDef_Slot——确实存在一些缺点。最重要的缺点是:

  • 类型安全:void * 用于数据指针、函数指针和小型整数,需要进行类型转换,这在 C 中技术上是未定义行为——但在所有相关架构上实际可行。(例如:Py_tp_doc 标记字符串;Py_mod_gil 标记整数。)
  • 有限的向前兼容性:如果扩展提供了当前解释器未知的槽 ID,模块创建将失败。这使得使用“可选”功能(即仅在解释器支持时才应生效的功能)变得麻烦。(最近添加的 Py_mod_gilPy_mod_multiple_interpreters 槽是很好的例子。)

    一种变通方法是在导出函数中检查 Py_Version,并返回适合当前解释器的槽数组。

更新默认值

通过新的 API,我们可以更新 Py_mod_multiple_interpretersPy_mod_gil 槽的默认值。

inittab

我们需要在 inittab 中允许不带 PyModuleDef 的槽——也就是说,添加一个 PyImport_ExtendInittab 的新变体。这应该作为本 PEP 的一部分吗?

inittab 用于嵌入,在其中通用/稳定的 ABI 并不那么重要。因此,将其留待以后更改可能没问题。

脚注


来源: https://github.com/python/peps/blob/main/peps/pep-0793.rst

最后修改: 2025-10-16 14:03:05 GMT