定义扩展模块

针对 CPython 的 C 扩展就是一个共享库(例如,Linux 上的 .so 文件,Windows 上的 .pyd DLL),它可被加载到 Python 进程中(例如,它可以用兼容的编译器设置进行编译),而这会导出一个 导出钩子 函数 (或旧式的 初始化函数)。

要在默认情况下可被导入 (也就是说,通过 importlib.machinery.ExtensionFileLoader),共享库必须在 sys.path 中可用,并且必须命名为模块名之后加一个在 importlib.machinery.EXTENSION_SUFFIXES 中列出的扩展名。

备注

构建、打包和分发扩展模块最好使用第三方工具完成,并且超出了本文的范围。一个合适的工具是 Setuptools,其文档可以在 https://setuptools.pypa.io/en/latest/setuptools.html 上找到。

扩展导出钩子

在 3.15 版本加入: 对 PyModExport_<name> 导出钩子的支持是在 Python 3.15 中增加的。 定义模块的旧方式仍然可用:如果你计划支持更早的 Python 版本可以查阅 PyInit 函数 一节或本文档的较早版本。

导出钩子必须是具有以下签名的导出函数:

PySlot *PyModExport_modulename(void)

对于具有仅限 ASCII 名称的模块,导出钩子 必须被命名为 PyModExport_<name>,其中 <name> 以模块名称替换。

对于非 ASCII 模块名称,则导出钩子必须被命名为 PyModExportU_<name> (注意增加了 U),其中 <name> 使用 Python 的 punycode 编码格式进行编码并将连字符替换为下划线。 在 Python 中:

def hook_name(name):
    try:
        suffix = b'_' + name.encode('ascii')
    except UnicodeEncodeError:
        suffix = b'U_' + name.encode('punycode').replace(b'-', b'_')
    return b'PyModExport' + suffix

导出钩子将返回由 PySlot 条目组成的数组,以一个槽位 ID 为 0 的条目作为终结。 这些槽位描述了模块应当如何被创建和初始化。

这个数组必须保持可用与恒定直到解释器关闭。 通常,它应当使用 static 存储。 建议针对任何动态行为使用 Py_mod_create 和 Py_mod_exec 槽位。

导出钩子可以返回 NULL 并设置一个异常来发出失败信号。

建议使用一个辅助宏来定义导出钩子函数:

PyMODEXPORT_FUNC
This macro is available when compiling for the 稳定 ABI 自 3.15 版起.

声明一个扩展模块导出钩子。 这个宏:

  • 指定 PySlot* 返回类型,

  • 添加平台所需的任何特殊链接声明,以及

  • 对于 C++,将函数声明为 extern "C"。

例如,一个名为 spam 的模块可以这样定义:

PyABIInfo_VAR(abi_info);

static PySlot spam_slots[] = {
    PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
    PySlot_STATIC_DATA(Py_mod_name, "spam"),
    PySlot_FUNC(Py_mod_exec, spam_init_function),
    ...
    PySlot_END
};

PyMODEXPORT_FUNC
PyModExport_spam(void)
{
    return spam_slots;
}

导出钩子通常是定义于模块 C 源代码中唯一的非 static 条目。

此钩子应当保持简短。 如果它在 return 一个静态数组之个还做了其他事,则需注意几点:

  • 如果你需要使用任何 Python C API,在常见的 ABI 不匹配情况下,建议先调用 PyABIInfo_Check() 来引发异常,而不是让程序崩溃。

  • 导出钩子中的代码绝不可依赖于 GIL: Python 的 自由线程构建版 只能在钩子返回后检查 Py_mod_gil 槽位(或确定其不存在)。

  • 类似地,此钩子可在任意子解释器中被调用,因为对 Py_mod_multiple_interpreters 槽位的存在(或不存在)的检查只在钩子返回后才执行。

例如:

PyMODEXPORT_FUNC
PyModExport_modulename(void)
{
   if (PyABIInfo_Check(&abi_info, "modulename") < 0) {
      /* ABI mismatch. It's not safe to examine the raised exception. */
      return NULL;
   }

   /* use Python API (as little as possible); don't rely on GIL */

   return modulename_slots;
}

备注

通过定义多个导出钩子,可以从同一个共享库中导出多个模块。然而,导入这些模块需要自定义导入器,或具备合适名称的扩展文件副本/链接,因为 Python 的导入机制只会找到与文件名相对应的函数。详情请参阅 PEP 489 中的 Multiple modules in one library 部分。

多阶段初始化

创建扩展模块的过程包含以下几个阶段:

  • Python 会查找并调用导出钩子,以获取有关如何创建该模块的信息。

  • 在执行任何实质性代码之前,Python 可以确定该模块支持哪些功能,并可以调整环境或拒绝加载不兼容的扩展。诸如 Py_mod_abi、Py_mod_gil 和 Py_mod_multiple_interpreters 之类的槽位会影响这一步骤。

  • 默认情况下,Python 随后会创建模块对象——也就是说,它在创建对象时相当于调用 __new__()。这一步可以通过 Py_mod_create 槽位进行覆盖。

  • Python 会为模块设置诸如 __package__ 和 __loader__ 之类的初始属性,并将模块对象插入到 sys.modules 中。

  • 随后,模块对象会以一种与扩展相关的特定方式完成初始化——这相当于创建对象时执行 __init__(),或相当于在 Python 语言模块中执行顶层代码。其行为通过 Py_mod_exec 槽位进行定义。

这被称为*多阶段初始化*,用以将其与传统的(但仍受支持的) single-phase initialization 区分开来;在后者中,初始化函数会返回一个已完全构建好的模块。

在 3.5 版本发生变更: 增加了对多阶段初始化的支持 (PEP 489)。

多个模块实例

默认情况下,扩展模块并非单例。例如,若移除 sys.modules 条目并重新导入该模块,则会创建一个新的模块对象;通常,该对象会被填充全新的方法和类型对象。旧模块则遵循常规的垃圾回收机制。这与纯 Python 模块的行为一致。

额外的模块实例可能会在 子解释器 中或者 Python 运行时重新初始化之后 (Py_Finalize() 和 Py_Initialize()) 被创建。在这些情况下,模块实例间共享 Python 对象可能导致程序崩溃或未定义的行为。

为避免这种问题,每个扩展模块的实例都应当是 隔离的: 对一个实例的修改不应隐式地影响其他的实例,以及模块所拥有的全部状态,包括对 Python 对象的引用,都应当是特定模块实例专属的。请参阅 隔离扩展模块 了解更多的细节和实用的指南。

一个避免这些问题的简单方式是 针对重复的初始化引发一个错误。

所有模块都应当支持 子解释器,否则就要显式地发出缺乏支持的信号。 这往往是通过隔离或阻止重复的初始化,如上文所述。一个模块也可以使用 Py_mod_multiple_interpreters 槽位将其限制于主解释器中。

PyInit 函数

自 3.15 版起已处于 Soft deprecated 状态: 该功能不会获得新功能,但目前暂无将其移除的计划。

与 PyModExport_modulename() 不同,扩展模块可以定义一种采用旧式签名的 initialization function,其签名如下:

PyObject *PyInit_modulename(void)

其名称应为 PyInit_<name>,其中 <name> 应替换为模块名称。对于非 ASCII 模块名称,则应使用 PyInitU_<name>;此时 <name> 的编码方式应与 export hook 相同(即使用带下划线的 Punycode)。

如果某个模块同时导出 PyInit_<name> 和 PyModExport_<name>,则 PyInit_<name> 函数将被忽略。

与 PyMODEXPORT_FUNC 类似,建议使用辅助宏来定义初始化函数:

PyMODINIT_FUNC

声明一个扩展模块初始化函数。这个宏:

  • 指定了 PyObject* 返回类型,

  • 添加平台所需的任何特殊链接声明,以及

  • 对于 C++,将函数声明为 extern "C"。

通常,初始化函数 (PyInit_modulename) 会返回一个包含非 NULL m_slots 的 PyModuleDef 实例。这使得 Python 能够使用 multi-phase initialization。

在将其返回之前,必须使用以下函数对 PyModuleDef 实例进行初始化:

PyObject *PyModuleDef_Init(PyModuleDef *def)
属于 稳定 ABI 自 3.5 版起.

确保模块定义是一个正确初始化的 Python 对象,并正确报告其类型以及引用计数。

返回强制转换为 PyObject* 的 def,或者如果出现错误则返回 NULL。

在模块初始化函数中返回 PyModuleDef 之前,必须调用此函数。不应在其他上下文中使用此函数。

请注意 Python 会假定 PyModuleDef 结构体是静态分配的。此函数可以返回一个新引用或借用引用;这个引用不可被释放。

在 3.5 版本加入.

例如,一个名为 spam 的模块可以这样定义:

static struct PyModuleDef spam_module = {
    .m_base = PyModuleDef_HEAD_INIT,
    .m_name = "spam",
    ...
};

PyMODINIT_FUNC
PyInit_spam(void)
{
    return PyModuleDef_Init(&spam_module);
}

旧式的单阶段初始化

自 3.15 版起已处于 Soft deprecated 状态: 单阶段初始化是一种用于初始化扩展模块的旧机制,它具有已知的缺点和设计瑕疵。建议扩展模块作者改用多阶段初始化。

不过,目前暂无计划取消对其的支持。

在单阶段初始化过程中,旧式 initialization function (PyInit_modulename) 应创建、填充并返回一个模块对象。通常通过 PyModule_Create() 以及 PyModule_AddObjectRef() 等函数来实现这一操作。

单阶段初始化与 默认方式 的主要区别如下:

  • 将 PyModuleDef.m_size 设置为 -1 的单相模块,或者更准确地说,是 包含 “单例”的模块。

    当此类模块首次完成初始化时,Python 会保存该模块的 __dict__ 内容(通常即该模块中的函数和类型)。

    对于后续的导入操作,Python 不会再次调用初始化函数;相反,它会创建一个带有新 __dict__ 的新模块对象,并将已保存的内容复制到其中。模块通过 (module_name,file_name) 元组与已保存的内容进行匹配,其中:

    作为首次初始化的一部分,Python 还会以该模块的名称将其添加到 sys.modules 中。例如,假设有一个单相位模块 _testsinglephase [1],该模块定义了一个函数 sum 和一个异常类 error:

    >>> import sys
    >>> import _testsinglephase as one
    >>> del sys.modules['_testsinglephase']
    >>> import _testsinglephase as two
    >>> one is two
    False
    >>> one.__dict__ is two.__dict__
    False
    >>> one.sum is two.sum
    True
    >>> one.error is two.error
    True
    

    该具体行为应被视为 CPython 的实现细节。

  • 对于具有非负 PyModuleDef.m_size 的单相模块,Python 可能会像对待 m_size == -1 那样,在 (module_name,file_name) 键下缓存一个指向初始化函数的指针。

    后续指定不同初始化函数的导入(例如使用 PyImport_CreateModuleFromInitfunc())可能会调用缓存函数,也可能调用所提供的“新”函数。

    具体行为应再次被视为 CPython 的实现细节。

  • 为解决 PyInit_modulename 函数不接受 spec 参数的限制,导入机制会保存部分状态,并在 PyInit_modulename 调用期间将其应用于首个匹配的模块对象。具体表现为:当导入子模块时,该机制会将父包名称自动前置到模块名前。

    单阶段 PyInit_modulename 函数应当尽早创建"其所属"模块对象,该操作需在任何其他模块对象创建之前完成。

  • 不支持非 ASCII 模块名 (PyInitU_modulename)。

  • 单阶段模块支持模块查找函数如 PyState_FindModule()。

  • 该模块的 PyModuleDef.m_slots 必须为 NULL。