
CPython 扩展模块定义指南PyModExport 导出钩子、PyInit 初始化函数与多阶段初始化【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython本文基于 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 上的.pydDLL它需要满足两个条件才能被导入可加载编译配置与 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 bU_ name.encode(punycode).replace(b-, b_) return bPyModExport 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_create与Py_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_name、Py_mod_doc、Py_mod_methods、Py_mod_abi等PySlot_FUNC(NAME, VALUE)函数槽用于Py_mod_create、Py_mod_exec、Py_mod_init等需要函数指针的槽位PySlot_DATA(NAME, VALUE)/PySlot_SIZE/PySlot_INT64/PySlot_UINT64其他载荷类型的构造方式PySlot_END数组终止符展开为{0}PySlot_PTR/PySlot_PTR_STATIC面向不支持 designated initializer 的旧版 CC11 及以下的等价写法钩子可以返回NULL并设置异常来表示加载失败。2.3 用PyMODEXPORT_FUNC宏声明钩子文档推荐用辅助宏PyMODEXPORT_FUNC声明导出钩子。从 Include/exports.h 可以看到它的定义#ifndef PyMODEXPORT_FUNC #define PyMODEXPORT_FUNC _PyINIT_FUNC_DECLSPEC PySlot* #endif该宏完成三件事指定返回类型PySlot*添加平台所需的外部链接声明Windows/Cygwin 等平台依赖__declspec机制见exports.h顶部的HAVE_DECLSPEC_DLL处理对 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_namesystem, .ml_methspam_system, .ml_flagsMETH_O, .ml_docExecute 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.4Py_mod_abi槽位与 ABI 检查上述示例中的PyABIInfo_VAR(abi_info)是 3.15 引入的 ABI 信息机制定义在 Include/modsupport.htypedef 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. Its not safe to examine the raised exception. */ return NULL; } /* use Python API (as little as possible); dont rely on GIL */ return modulename_slots; }2.6 单个共享库中导出多个模块通过定义多个导出钩子一个共享库可以导出多个模块。但由于 Python 导入机制只会查找与文件名对应的那个符号导入这些模块需要自定义导入器或者提供适当命名的扩展文件副本/链接。这一行为对应 PEP 489 中「Multiple modules in one library」一节描述的规则。3. 多阶段初始化Multi-phase initialization创建扩展模块的过程分为若干阶段这就是「多阶段初始化」名称的由来Python 找到并调用导出钩子获取创建模块所需的信息在任何实质代码执行之前Python 即可判断模块支持哪些能力并据此调整环境或拒绝加载不兼容的扩展。Py_mod_abi、Py_mod_gil、Py_mod_multiple_interpreters等槽位影响这一步默认情况下由Python 自己创建模块对象——等价于创建对象时调用object.__new__。可用Py_mod_create槽位覆盖此步骤Python 设置__package__、__loader__等初始模块属性并把模块对象插入sys.modules之后模块以扩展特有的方式完成初始化——等价于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_namePyInit_name会被忽略——这与第 2.1 节提到的 importdl.c 查找优先级一致。4.1PyMODINIT_FUNC声明宏与PyMODEXPORT_FUNC类似文档推荐用PyMODINIT_FUNC宏声明初始化函数。从 Include/exports.h 看其定义是#ifndef PyMODINIT_FUNC #define PyMODINIT_FUNC _PyINIT_FUNC_DECLSPEC PyObject* #endif即指定PyObject*返回类型、添加平台链接声明、对 C 声明extern C。4.2PyModuleDef与PyModuleDef_Init通常PyInit_modulename返回一个m_slots非NULL的PyModuleDef实例让 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_Create与PyModule_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模块对象one与two不是同一个__dict__也不是同一个但模块内的函数sum与异常类error是同一对象。文档同时声明该精确行为应视为 CPython 的实现细节。2spec 参数的替代机制。由于PyInit_modulename不接受spec参数导入器把部分导入机制状态保存起来并应用到该调用期间创建的第一个合适模块上——具体来说导入子模块时会把父包名前缀拼接到模块名上。因此单阶段的PyInit_modulename应当在可能创建任何其他模块对象之前尽快创建「属于自己」的模块对象。3不支持非 ASCII 模块名即PyInitU_modulename形式不可用于单阶段初始化。4单阶段模块支持PyState_FindModule之类的模块查找函数多阶段模块不支持该旧式查找。5PyModuleDef.m_slots必须为NULL——这是区分单阶段与多阶段路径的标志字段。6. 多个模块实例隔离要求与旧式单阶段模块不同默认情况下扩展模块不是单例如果从sys.modules删除条目后重新导入会创建一个新模块对象并通常装入全新的方法与类型对象旧模块按正常垃圾回收处理——这与纯 Python 模块的行为一致。在子解释器sub-interpreter或 Python 运行时重新初始化Py_FinalizePy_Initialize之后也可能存在额外的模块实例。在这些场景下在模块实例之间共享 Python 对象很可能导致崩溃或未定义行为。为此每个扩展模块实例都应当是隔离的isolated对某个实例的修改不应隐式影响其他实例模块拥有的所有状态包括对 Python 对象的引用都应当属于特定模块实例。官方 how-to 文档 Doc/howto/free-threading-extensions.rst 及相关的隔离实践指南可作进一步参考一种更简单的规避方式是在重复初始化时直接抛错彻底放弃多实例所有模块都应预期支持子解释器或者显式声明不支持——通常通过上述隔离或阻止重复初始化实现模块也可以借助Py_mod_multiple_interpreters槽位把自己限定在主解释器内。7. 在仓库中验证与延伸阅读导出钩子 槽位表的真实用例可参考 Modules/_testmultiphase.c多阶段初始化测试模块与 Modules/xxlimited.c受限于 stable ABI 的 xx 模块测试数据位于 Lib/test/test_cext/extension.c。符号查找与钩子解析的完整实现见 Python/importdl.c 与 Python/import.c。想动手写第一个扩展模块官方教程 Doc/extending/first-extension-module.rst 与本文引用的 Doc/includes/capi-extension/spammodule-01.c 保持了内容同步。ABI 版本与导出符号的约定见 Doc/c-api/apiabiversion.rst3.15 的变化说明见 Doc/whatsnew/3.15.rst。选型小结面向 3.15 的新代码应直接使用PyModExport_name导出钩子 静态PySlot数组 Py_mod_abi/Py_mod_create/Py_mod_exec槽位需要兼容旧版本 Python 时使用PyMODINIT_FUNC PyInit_name返回经PyModuleDef_Init处理的PyModuleDefm_slots非空即走多阶段路径单阶段初始化仅作为遗留机制存在新扩展不建议采用。【免费下载链接】cpythonThe Python programming language项目地址: https://gitcode.com/GitHub_Trending/cp/cpython创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考