CPython Windows 扩展模块构建指南:MSVC 编译、DLL 导入库与 Py_NO_LINK_LIB 机制
本篇基于 CPython 仓库官方文档 Doc/extending/windows.rst 展开,系统讲解在 Windows 上使用 Microsoft Visual C++ 构建 Python C/C++ 扩展模块的完整方法:包括 Unix 与 Windows 动态链接范式的本质差异、cl 编译器的两条链接路径(隐式链接与 Py_NO_LINK_LIB 手动链接),以及符号导出和导入库裁剪等实战技巧。读完本文,你将能够独立在 Windows 上编译出可被 Python 加载的 .pyd(即 DLL)扩展模块,并理解 PC/pyconfig.h 中自动选择 pythonXY.lib 的底层逻辑。
扩展模块的两种构建方式
文档开篇即给出两条路线,这与 Unix 平台上的做法一一对应:
- 推荐路线:用
setuptools控制构建过程。 对绝大多数扩展模块,setuptools方案工作良好。你仍然需要当初构建 Python 所用的 C 编译器,通常是 Microsoft Visual C++。 - 手动路线:直接调用编译器。 如果确有需要手动操作,文档建议研究标准库
winsound模块的工程文件 PCbuild/winsound.vcxproj 作为参照。该工程文件引用了 PC/winsound.c 作为编译目标(见工程内<ClCompile Include="..\PC\winsound.c" />),是 CPython 仓库中现成的 Windows 扩展模块范例。
文档还提醒:模块作者应优先采用 distutils/setuptools 方式构建扩展模块,而不是手写编译命令。
关于版本号约定:文档中出现的文件名包含编码后的 Python 版本号 XY,其中 X 为主版本号、Y 为次版本号(例如 Python 2.2.1 对应 22)。就本仓库当前代码而言,Include/patchlevel.h 中定义为 PY_MAJOR_VERSION 3、PY_MINOR_VERSION 16,因此在当前主线分支上 XY 即为 316,对应 python316.lib、python316_d.lib 等文件。
Unix 与 Windows 的动态链接差异
文档强调:Unix 与 Windows 在运行时加载代码上采用完全不同的范式,构建任何动态加载模块之前,必须先理解本系统的机制。
共享对象(.so)与动态链接库(.dll)
- Unix:共享对象
.so文件既包含供程序使用的代码,也包含它所期望在宿主程序中找到的一组函数和数据的名字。当.so被并入程序时,文件中所有对这些函数和数据的引用都会被改写,指向函数和数据在内存中的实际位置——这本质上就是一次链接操作(运行时重定位)。 - Windows:
.dll文件不存在任何悬空引用。对函数或数据的访问通过一张查找表完成:DLL 代码本身无需在运行时修正以指向宿主程序内存,代码一开始就通过 DLL 的查找表访问,运行时被修改的是查找表本身,让它指向真正的函数和数据。
静态库与导入库(都是 .lib)
- Unix:只有一种库文件
.a,它包含多个目标文件(.o)中的代码。生成.so的链接阶段,链接器遇到无法解析的标识符时,会去各库的目标文件中查找;一旦找到,就会把该目标文件的全部代码包含进来。 - Windows:有两种库,静态库和导入库,扩展名都是
.lib。- 静态库类似 Unix 的
.a,包含按需引入的代码; - 导入库的作用基本只是“安抚”链接器——确认某个标识符是合法的、且 DLL 加载时必然存在于程序里。链接器利用导入库中的信息,为 DLL 自身不包含的标识符构建查找表。当某个应用程序或 DLL 被链接时,可能生成一个导入库,所有未来依赖这些符号的 DLL 都必须使用它。
- 静态库类似 Unix 的
文档用一个共享代码块 A 的经典例子说明了两者的关键区别:假设要构建两个动态加载模块 B 和 C,它们都依赖另一块代码 A。
- 在 Unix 上,你不会把
A.a传给B.so和C.so的链接器——否则 A 会被包含两次,B 和 C 各自持有一份副本。 - 在 Windows 上,构建
A.dll时会同时生成A.lib。你要把A.lib传给 B 和 C 的链接器。A.lib不包含代码,只包含运行时访问 A 的代码所需的信息。
文档给出的记忆类比非常精妙:在 Windows 上使用导入库有点像 import spam——让你访问 spam 的名字,但不产生额外副本;在 Unix 上链接一个库则更像 from spam import *——它确实会创建一份独立副本。
Py_NO_LINK_LIB 宏
文档定义了新的编译期开关(CPython 3.14 起引入):
Py_NO_LINK_LIB:关闭 CPython 头文件内部通过隐式#pragma机制与 Python 库的链接行为。
在源码层面,该机制的完整实现位于 PC/pyconfig.h(Windows 平台编译时生效的头文件),逻辑如下:
/* Automatic linking of extension python3x.lib files for MSVC DLLs.
This lets MSVC users build extensions without manually specifying .lib files.
Define Py_NO_LINK_LIB to disable this behavior. */
#if !defined(Py_NO_LINK_LIB) \
&& defined(_MSC_VER) && defined(Py_ENABLE_SHARED) \
&& !defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_BUILTIN)
/* not building the core - must be an ext */
# if defined(Py_GIL_DISABLED)
# if defined(Py_DEBUG)
# pragma comment(lib,"python316t_d.lib")
# elif defined(Py_LIMITED_API) || defined(Py_TARGET_ABI3T)
# pragma comment(lib,"python3t.lib")
# else
# pragma comment(lib,"python316t.lib")
# endif /* Py_DEBUG */
# else
# if defined(Py_DEBUG)
# pragma comment(lib,"python316_d.lib")
# elif defined(Py_TARGET_ABI3T)
# pragma comment(lib,"python3t.lib")
# elif defined(Py_LIMITED_API)
# pragma comment(lib,"python3.lib")
# else
# pragma comment(lib,"python316.lib")
# endif /* Py_DEBUG */
# endif /* Py_GIL_DISABLED */
#endif
从这段源码结构看,自动链接有四个前提条件:未定义 Py_NO_LINK_LIB、使用 MSVC(_MSC_VER)、启用了共享核心(Py_ENABLE_SHARED,见同文件 PC/pyconfig.h 中 Py_NO_ENABLE_SHARED 的处理)、并且不是在构建核心本身(排除 Py_BUILD_CORE / Py_BUILD_CORE_BUILTIN)。在此基础上,头文件按构建配置自动选择导入库:
| 构建配置 | 选中的导入库(本仓库 3.16 主线为例) |
|---|---|
| Debug | python316_d.lib |
| Release(默认) | python316.lib |
Release + Limited API(Py_LIMITED_API) |
python3.lib(不带版本号的稳定 ABI 导入库) |
GIL 禁用构建(Py_GIL_DISABLED) |
python316t_d.lib / python3t.lib / python316t.lib(带 t 后缀) |
这就是文档所说“头文件为 Debug 选 pythonXY_d.lib、Release 选 pythonXY.lib、启用 Limited API 的 Release 选 pythonX.lib”的实现细节。
实战:在 Windows 上使用 DLL
文档提醒:Windows 版 Python 使用 Microsoft Visual C++ 构建,使用其他编译器可能有效也可能无效,以下内容为 MSVC 专属。在 Windows 上创建 DLL 时,使用 CPython 库有两种方式:
方式一:默认隐式链接
包含 PC/pyconfig.h(直接或间接经由 Python.h)会触发一次隐式的、配置感知的库链接。构建两个 DLL——spam 和依赖 spam 中 C 函数的 ni——可用以下命令:
cl /LD /I/python/include spam.c
cl /LD /I/python/include ni.c spam.lib
(其中 /I 参数应指向你的 Python 安装头文件目录,/LD 表示生成 DLL。)
- 第一条命令生成三个文件:
spam.obj、spam.dll、spam.lib。spam.dll本身不包含任何 Python 函数(如PyArg_ParseTuple),但正是由于隐式链接了pythonXY.lib,它知道如何找到 Python 代码。 - 第二条命令生成
ni.dll(以及.obj和.lib),它知道如何找到 spam 中以及 Python 可执行文件中需要的函数。
方式二:定义 Py_NO_LINK_LIB 手动链接
在包含 Python.h 之前定义 Py_NO_LINK_LIB 宏(通过 /D 传入预处理器),此时你必须自行把 pythonXY.lib 传给链接器:
cl /LD /DPy_NO_LINK_LIB /I/python/include spam.c ../libs/pythonXY.lib
cl /LD /DPy_NO_LINK_LIB /I/python/include ni.c spam.lib ../libs/pythonXY.lib
两条命令的产物与含义与方式一完全相同(spam.obj/spam.dll/spam.lib,随后是 ni.dll),区别仅在于 Python 库导入库的来源从头文件里的 #pragma comment 变成了命令行上的显式参数。方式二的好处是链接目标完全透明、可被脚本精确控制,代价是必须保证 .lib 路径正确。
导出你自己的符号
并非所有标识符都会被导出到查找表。如果你希望其他模块(包括 Python 本体)能看到你的标识符,必须显式标注 _declspec(dllexport),例如:
void _declspec(dllexport) initspam(void)
PyObject _declspec(dllexport) *NiGetSpamData(void)
注意 Windows 的 CPython 通过 __declspec 处理符号可见性,PC/pyconfig.h 中对所有使用该头文件的 Windows 编译器统一定义了 HAVE_DECLSPEC_DLL,配合 Include/pyport.h 中的 PyAPI_FUNC/PyAPI_DATA 等宏完成核心的符号导出。
裁剪多余的默认导入库
文档最后给出了一个实用技巧:Developer Studio 会塞进大量你根本不需要的导入库,给可执行文件凭空增加约 100K。去除方法:打开 Project Settings 对话框的 Link 选项卡,勾选 ignore default libraries,并在库列表中添加正确的 msvcrt{xx}.lib(CRT 导入库),让链接器只保留必需的那一个。
配套参考:从仓库内查看构建细节
- 标准库
winsound的 Windows 扩展模块工程文件:PCbuild/winsound.vcxproj,可对照其ClCompile、Link与附加库配置理解手动构建参数。 - CPython 在 Windows 上自身的构建说明(MSVC 工作负载、
build.bat、Release/Debug 配置、_d后缀约定、Clang/LLVM 备选工具链等):PCbuild/readme.txt。其中 Debug 配置会在二进制名中加入_d(如python_d.exe),与本文 Debug 模式选择pythonXY_d.lib的规则相互呼应。 - 跨平台扩展模块构建总览(setuptools/distutils 路线、构建选项):Doc/extending/building.rst。
- 最小可运行的扩展模块教程(模块样板代码、
PyInit_xxx入口):Doc/extending/first-extension-module.rst。
小结
在 Windows 上构建 CPython 扩展模块的核心心智模型是:.pyd 就是一份 DLL,它不重定位、而是通过导入库(.lib)构建的查找表在运行时定位 Python 核心符号;CPython 头文件默认替你完成这一步(按 Debug/Release/Limited API/GIL 状态自动选择 pythonXY.lib),而 Py_NO_LINK_LIB 让你拿回控制权、显式指定导入库。配合 _declspec(dllexport) 导出自己的符号、以及 ignore default libraries 裁剪冗余 CRT 库,即可产出一个尺寸干净、链接关系明确的 Windows 扩展模块。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0625
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00