ctypes 调用 C++ 动态库:从编译到踩坑
在 Python 项目里遇到性能瓶颈时,把核心计算用 C++ 写成动态库再用 ctypes 调用,是个省事的方案。不需要额外的编译工具链,Windows 上的 DLL 直接就能用。但是这里面细节不少,比如符号修饰、调用约定、类型映射,很容易踩坑。
编译 C++ DLL 的关键点
C++ 支持函数重载,编译时会通过名称修饰把函数名和参数类型编码成唯一的符号。这让 Python 的 ctypes 很难直接找到原始函数名。解决办法很简单:用 extern "C" 包裹导出的函数,强制编译器按 C 的链接规则生成符号。
extern "C" {
void c_function(); // 链接时查找未修饰符号 c_function
}
导出函数有两种常见方式。__declspec(dllexport) 直接写在声明前,适合少量函数:
extern "C" __declspec(dllexport) int Add(int a, int b) {
return a + b;
}
如果导出函数很多,用 .def 文件集中管理更清晰:
LIBRARY MyLib
EXPORTS
Add
这两种方式各有侧重,下表总结了差异:
| 特性 | __declspec(dllexport) | .def 文件 |
|---|---|---|
| 可读性 | 高(内联声明) | 中(需切换文件) |
| 维护性 | 低(分散) | 高(集中管理) |
| 兼容性 | 依赖编译器 | 强(标准格式) |
编译出 DLL 后,可以用 Dependency Walker 确认导出函数是否正常。加载 DLL 后,查看'Exported Functions'区域,如果函数名被 C++ 修饰过(如 ?func@@YAXH@Z),说明忘了加 extern "C"。
Python 端的 ctypes 调用流程
用 ctypes 加载 DLL 时要注意调用约定。Windows API 通常使用 stdcall(被调用者清栈),而 C/C++ 库默认 cdecl(调用者清栈)。加载时分别使用 WinDLL 和 CDLL:
from ctypes import CDLL, WinDLL
cdecl_lib = CDLL("example_cdecl.dll") # cdecl 约定
stdcall_lib = WinDLL()

