首页
/ CPython Windows 扩展模块构建指南:MSVC 编译、DLL 导入库与 Py_NO_LINK_LIB 机制

CPython Windows 扩展模块构建指南:MSVC 编译、DLL 导入库与 Py_NO_LINK_LIB 机制

2026-09-06 15:37:07作者:温玫谨Lighthearted

本篇基于 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 平台上的做法一一对应:

  1. 推荐路线:用 setuptools 控制构建过程。 对绝大多数扩展模块,setuptools 方案工作良好。你仍然需要当初构建 Python 所用的 C 编译器,通常是 Microsoft Visual C++。
  2. 手动路线:直接调用编译器。 如果确有需要手动操作,文档建议研究标准库 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 3PY_MINOR_VERSION 16,因此在当前主线分支上 XY 即为 316,对应 python316.libpython316_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 都必须使用它

文档用一个共享代码块 A 的经典例子说明了两者的关键区别:假设要构建两个动态加载模块 B 和 C,它们都依赖另一块代码 A。

  • Unix 上,你不会A.a 传给 B.soC.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.hPy_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.objspam.dllspam.libspam.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,可对照其 ClCompileLink 与附加库配置理解手动构建参数。
  • 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 扩展模块。

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