首页
/ CPython 扩展模块定义指南:PyModExport 导出钩子、PyInit 初始化函数与多阶段初始化

CPython 扩展模块定义指南:PyModExport 导出钩子、PyInit 初始化函数与多阶段初始化

2026-09-06 21:34:07作者:邬祺芯Juliet

本文基于 CPython 官方 C API 文档 extension-modules.rst 编写,完整覆盖 CPython 扩展模块的两种定义方式——3.15 新增的 PyModExport_<name> 导出钩子(export hook)与旧式的 PyInit_<name> 初始化函数,并深入讲解多阶段初始化(multi-phase initialization)的各阶段语义、多个模块实例的隔离要求,以及旧式单阶段初始化的行为差异与已知设计缺陷。读完后,你将能够按当前 CPython 源码的标准写出可被 import 机制正确加载的 C 扩展模块,并理解每种初始化方式在 ABI、GIL、子解释器场景下的适用边界。

1. 什么是 CPython 扩展模块

CPython 扩展模块本质上是一个共享库(Linux 上的 .so、Windows 上的 .pyd DLL),它需要满足两个条件才能被导入:

  • 可加载:编译配置与 Python 解释器兼容,能装入 Python 进程;
  • 可发现:共享库位于 sys.path 上,且文件名是「模块名 + importlib.machinery.EXTENSION_SUFFIXES 中列出的某个扩展名」,这样默认导入器 importlib.machinery.ExtensionFileLoader 才能找到它。

文档明确建议:扩展模块的构建、打包与分发最好交给第三方工具(如 Setuptools)完成,这属于本文档之外的范畴。本文聚焦于「模块本身的定义与初始化」这一核心问题。

2. 新式定义:PyModExport 导出钩子(3.15 起)

Python 3.15 引入了 PyModExport_<name> 导出钩子。计划支持更早版本 Python 的模块仍可使用旧式 PyInit 方式(见第 4 节),两者在本仓库中均受支持。

导出钩子是一个导出的 C 函数,签名如下:

PySlot *PyModExport_modulename(void);

2.1 函数命名规则:ASCII 与非 ASCII 模块名

  • 仅含 ASCII 字符的模块名:钩子函数命名为 PyModExport_{<name>}<name> 直接替换为模块名。
  • 非 ASCII 模块名:钩子函数改为 PyModExportU_{<name>}(注意多了一个 U),其中 <name> 使用 Python 的 punycode 编码,并将连字符替换为下划线。文档给出的换算逻辑为:
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

这一命名逻辑在 CPython 源码中有对应实现:Python/importdl.c 中定义了四组前缀常量——ASCII 前缀 {"PyInit", "PyModExport"} 与非 ASCII 前缀 {"PyInitU", "PyModExportU"},导入器先用 ASCII 编码模块名,失败并捕获 PyUnicodeEncodeError 后回退到 PyUnicode_AsEncodedString(name, "punycode", NULL),再据此在共享库中查找对应符号。导入器优先查找导出钩子:只有当 PyModExport 符号不存在时才会退回 PyInit 符号。

2.2 返回值:PySlot 槽位数组

导出钩子返回一个 PySlot 条目数组,以 slot ID 为 0 的条目(即 PySlot_END)结尾。这些槽位描述模块应当如何被创建和初始化。关键约束:

  • 该数组必须保持有效且恒定,直到解释器关闭,通常应使用 static 存储;
  • 任何动态行为都应放到 Py_mod_createPy_mod_exec 槽位中,而不是让槽位数组本身变化。

PySlot 结构体与配套的填充宏定义在 Include/slots.h 中。从源码看,每个槽位是固定 16 字节的布局:sl_id(槽位标识)、sl_flags(标志位)加一个联合体载荷(指针 / 函数指针 / 整数):

struct PySlot {
    uint16_t sl_id;
    uint16_t sl_flags;
    uint32_t sl_reserved; // must be 0
    union {
        void *sl_ptr;
        void (*sl_func)(void);
        Py_ssize_t sl_size;
        int64_t sl_int64;
        uint64_t sl_uint64;
    };
};

常用构造宏(同样位于 Include/slots.h):

用途
PySlot_STATIC_DATA(NAME, VALUE) 静态数据槽(打上 PySlot_STATIC 标志),用于 Py_mod_namePy_mod_docPy_mod_methodsPy_mod_abi
PySlot_FUNC(NAME, VALUE) 函数槽,用于 Py_mod_createPy_mod_execPy_mod_init 等需要函数指针的槽位
PySlot_DATA(NAME, VALUE) / PySlot_SIZE / PySlot_INT64 / PySlot_UINT64 其他载荷类型的构造方式
PySlot_END 数组终止符,展开为 {0}
PySlot_PTR / PySlot_PTR_STATIC 面向不支持 designated initializer 的旧版 C++(C++11 及以下)的等价写法

钩子可以返回 NULL 并设置异常来表示加载失败。

2.3 用 PyMODEXPORT_FUNC 宏声明钩子

文档推荐用辅助宏 PyMODEXPORT_FUNC 声明导出钩子。从 Include/exports.h 可以看到它的定义:

#ifndef PyMODEXPORT_FUNC
    #define PyMODEXPORT_FUNC _PyINIT_FUNC_DECLSPEC PySlot*
#endif

该宏完成三件事:

  1. 指定返回类型 PySlot*
  2. 添加平台所需的外部链接声明(Windows/Cygwin 等平台依赖 __declspec 机制,见 exports.h 顶部的 HAVE_DECLSPEC_DLL 处理);
  3. 对 C++ 将函数声明为 extern "C"

完整示例——模块 spam 的导出钩子(对应 Doc/c-api/extension-modules.rst 中的示例):

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_init, spam_init_function),
    ...
    PySlot_END
};

PyMODEXPORT_FUNC
PyModExport_spam(void)
{
    return spam_slots;
}

文档强调:导出钩子通常应该是模块 C 源码中唯一非 static 的项

仓库中有一个更完整的可编译示例,即 C API 文档内嵌教程用的 Doc/includes/capi-extension/spammodule-01.c。它实现了一个带 spam.system(command) 函数的模块:

static PyMethodDef spam_methods[] = {
    {
        .ml_name="system",
        .ml_meth=spam_system,
        .ml_flags=METH_O,
        .ml_doc="Execute a shell command.",
    },
    {NULL, NULL, 0, NULL}        /* Sentinel */
};

PyABIInfo_VAR(abi_info);

static PySlot spam_slots[] = {
    PySlot_STATIC_DATA(Py_mod_abi, &abi_info),
    PySlot_STATIC_DATA(Py_mod_name, "spam"),
    PySlot_STATIC_DATA(Py_mod_doc, "A wonderful module with an example function"),
    PySlot_STATIC_DATA(Py_mod_methods, spam_methods),
    PySlot_END
};

PyMODEXPORT_FUNC PyModExport_spam(void);

PyMODEXPORT_FUNC
PyModExport_spam(void)
{
   return spam_slots;
}

2.4 Py_mod_abi 槽位与 ABI 检查

上述示例中的 PyABIInfo_VAR(abi_info) 是 3.15 引入的 ABI 信息机制,定义在 Include/modsupport.h

typedef struct PyABIInfo {
    uint8_t abiinfo_major_version;
    uint8_t abiinfo_minor_version;
    uint16_t flags;
    uint32_t build_version;
    uint32_t abi_version;
} PyABIInfo;

#define PyABIInfo_STABLE        0x0001
#define PyABIInfo_GIL           0x0002
#define PyABIInfo_FREETHREADED  0x0004
#define PyABIInfo_INTERNAL      0x0008

#define PyABIInfo_FREETHREADING_AGNOSTIC (PyABIInfo_GIL|PyABIInfo_FREETHREADED)

PyABIInfo_VAR(NAME) 宏以 _PyABIInfo_DEFAULT 展开为一个 static PyABIInfo 变量(默认值包含 PY_VERSION_HEX 构建版本与按 Py_LIMITED_API 计算的 ABI 版本);PyABIInfo_Check(info, module_name) 则用于在 ABI 不匹配时抛异常而不是崩溃。

2.5 钩子内的三条注意事项

导出钩子应当保持简短。如果钩子除了 return 一个静态数组之外还做了别的事,则有以下约束:

  • 若需要调用任何 Python C API,推荐先调用 PyABIInfo_Check,在常见的 ABI 不匹配场景下抛异常而非崩溃;
  • 钩子中的代码绝不能依赖 GIL——free-threaded 构建的 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;
}

2.6 单个共享库中导出多个模块

通过定义多个导出钩子,一个共享库可以导出多个模块。但由于 Python 导入机制只会查找与文件名对应的那个符号,导入这些模块需要自定义导入器,或者提供适当命名的扩展文件副本/链接。这一行为对应 PEP 489 中「Multiple modules in one library」一节描述的规则。

3. 多阶段初始化(Multi-phase initialization)

创建扩展模块的过程分为若干阶段,这就是「多阶段初始化」名称的由来:

  1. Python 找到并调用导出钩子,获取创建模块所需的信息;
  2. 在任何实质代码执行之前,Python 即可判断模块支持哪些能力,并据此调整环境或拒绝加载不兼容的扩展Py_mod_abiPy_mod_gilPy_mod_multiple_interpreters 等槽位影响这一步;
  3. 默认情况下由 Python 自己创建模块对象——等价于创建对象时调用 object.__new__。可用 Py_mod_create 槽位覆盖此步骤;
  4. Python 设置 __package____loader__ 等初始模块属性,并把模块对象插入 sys.modules
  5. 之后模块以扩展特有的方式完成初始化——等价于 object.__init__ 或执行 Python 模块的顶层代码,行为由 Py_mod_exec 槽位指定。

多阶段初始化由 PEP 489 定义,自 Python 3.5 起被支持;它与「初始化函数一次性返回构建完毕的模块」的旧式单阶段初始化相对(见第 5 节)。

4. 旧式定义:PyInit 初始化函数(3.15 起 soft-deprecated)

作为 PyModExport_<name> 的替代,扩展模块也可以定义旧式初始化函数

PyObject* PyInit_modulename(void);
  • 函数名为 PyInit_{<name>};非 ASCII 模块名改用 PyInitU_{<name>},编码方式与导出钩子相同(punycode + 下划线)。
  • 若同一模块同时导出 PyInit_<name>PyModExport_<name>PyInit_<name> 会被忽略——这与第 2.1 节提到的 importdl.c 查找优先级一致。

4.1 PyMODINIT_FUNC 声明宏

PyMODEXPORT_FUNC 类似,文档推荐用 PyMODINIT_FUNC 宏声明初始化函数。从 Include/exports.h 看,其定义是:

#ifndef PyMODINIT_FUNC
    #define PyMODINIT_FUNC _PyINIT_FUNC_DECLSPEC PyObject*
#endif

即:指定 PyObject* 返回类型、添加平台链接声明、对 C++ 声明 extern "C"

4.2 PyModuleDefPyModuleDef_Init

通常 PyInit_modulename 返回一个 m_slotsNULLPyModuleDef 实例,让 Python 走多阶段初始化路径。返回前必须用如下函数初始化该实例:

PyObject* PyModuleDef_Init(PyModuleDef *def);

该函数确保模块定义是一个正确报告类型与引用计数的 Python 对象,成功返回 def 转型为 PyObject*,出错返回 NULL。约束包括:它是从模块初始化函数返回 PyModuleDef 之前的必需调用,不应用于其他上下文;Python 假定 PyModuleDef 结构体是静态分配的;函数返回的引用可能是新引用也可能是借用引用,不得释放

示例——模块 spam 的旧式定义:

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

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

需要说明的是,PyInit 方式在 3.15 被标记为 soft-deprecated:不会再获得新特性,但也没有移除计划,因此兼容旧版本的模块仍可继续使用。

5. 旧式单阶段初始化的行为差异与缺陷

单阶段初始化(legacy single-phase initialization)同样被标记为 soft-deprecated:它是有已知缺点和设计缺陷的旧机制,扩展作者被鼓励改用多阶段初始化,但官方没有移除计划。

在单阶段初始化中,PyInit_modulename 应当创建、填充并返回模块对象,通常借助 PyModule_CreatePyModule_AddObjectRef 一类函数完成。它与默认的多阶段初始化的关键差异如下:

(1)单阶段模块实际上是「单例」。 首次初始化时,Python 会保存模块 __dict__ 的内容(即通常的函数与类型对象);后续导入时不再调用初始化函数,而是创建带新 __dict__ 的新模块对象,并把保存的内容拷贝进去。用 CPython 自测套件中的内部模块 _testsinglephase(源码见 Modules/_testsinglephase.c)演示:

>>> 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

模块对象 onetwo 不是同一个,__dict__ 也不是同一个,但模块内的函数 sum 与异常类 error同一对象。文档同时声明:该精确行为应视为 CPython 的实现细节。

(2)spec 参数的替代机制。 由于 PyInit_modulename 不接受 spec 参数,导入器把部分导入机制状态保存起来并应用到该调用期间创建的第一个合适模块上——具体来说,导入子模块时会把父包名前缀拼接到模块名上。因此单阶段的 PyInit_modulename 应当在可能创建任何其他模块对象之前尽快创建「属于自己」的模块对象

(3)不支持非 ASCII 模块名(即 PyInitU_modulename 形式不可用于单阶段初始化)。

(4)单阶段模块支持 PyState_FindModule 之类的模块查找函数(多阶段模块不支持该旧式查找)。

(5)PyModuleDef.m_slots 必须为 NULL——这是区分单阶段与多阶段路径的标志字段。

6. 多个模块实例:隔离要求

与旧式单阶段模块不同,默认情况下扩展模块不是单例:如果从 sys.modules 删除条目后重新导入,会创建一个新模块对象,并通常装入全新的方法与类型对象;旧模块按正常垃圾回收处理——这与纯 Python 模块的行为一致。

在子解释器(sub-interpreter)或 Python 运行时重新初始化(Py_Finalize + Py_Initialize)之后,也可能存在额外的模块实例。在这些场景下,在模块实例之间共享 Python 对象很可能导致崩溃或未定义行为。为此:

  • 每个扩展模块实例都应当是隔离的(isolated):对某个实例的修改不应隐式影响其他实例;模块拥有的所有状态(包括对 Python 对象的引用)都应当属于特定模块实例。官方 how-to 文档 Doc/howto/free-threading-extensions.rst 及相关的隔离实践指南可作进一步参考;
  • 一种更简单的规避方式是:在重复初始化时直接抛错,彻底放弃多实例;
  • 所有模块都应预期支持子解释器,或者显式声明不支持——通常通过上述隔离或阻止重复初始化实现;模块也可以借助 Py_mod_multiple_interpreters 槽位把自己限定在主解释器内。

7. 在仓库中验证与延伸阅读

选型小结:面向 3.15+ 的新代码应直接使用 PyModExport_<name> 导出钩子 + 静态 PySlot 数组 + Py_mod_abi/Py_mod_create/Py_mod_exec 槽位;需要兼容旧版本 Python 时使用 PyMODINIT_FUNC PyInit_<name> 返回经 PyModuleDef_Init 处理的 PyModuleDefm_slots 非空即走多阶段路径);单阶段初始化仅作为遗留机制存在,新扩展不建议采用。

登录后查看全文
热门项目推荐
相关项目推荐