前言
Python 写起来快,C++ 跑起来快,于是「把两者接起来」成了很常见的需求:用 Python 做脚本、数据处理和胶水层,把计算密集或需要复用现有库的部分交给 C++。但「相互调用」其实是两个方向、四套机制,很多文章把它们混在一起讲,读者照着抄就卡在ImportError或链接错误上。
第一个方向是 Python 调 C/C++:把 C++ 编译成扩展模块(extension module)供import,或者编译成普通动态库用ctypes加载。第二个方向是 C++ 调 Python:在 C++ 程序里嵌入(embed)Python 解释器,初始化后执行脚本、调用函数。两个方向的初始化、错误处理、内存管理规则完全不同。
还有一个常见的误解是「C++ 函数写好了,Python 就能直接调」。实际上 CPython 只认 C ABI 的模块入口PyInit_模块名,C++ 的命名修饰(name mangling)、异常、标准库对象都不能直接跨越边界;必须用extern "C"包一层,并把 C++ 异常转换成 Python 异常。本文按两个方向分别给出可读的示例,并说明 GIL、引用计数和构建配置。文中 API 以 CPython 3.x 文档为准,本机没有 Python 与 C++ 工具链,未做编译运行验证。
一、Python 调 C++:三条路线
| 路线 | 做法 | 优点 | 代价 |
|---|
| CPython C 扩展 | 实现PyInit_xxx,用 C API 解析参数 | 无第三方依赖,控制力最强 | 引用计数易错,样板代码多 |
ctypes | 把 C++ 编成 C ABI 动态库,Python 侧加载 | 不用写绑定代码,改一行编一次 | 只能传 C 兼容类型,回调与结构体要手工映射 |
| pybind11 等绑定库 | 用头文件库自动生成绑定 | 写法接近 C++,支持类与 STL | 引入第三方依赖,模板报错较长 |
先看ctypes,它最适合快速验证。C++ 侧只需暴露一个 C 链接的函数:
// 适用于 C++17;Linux 下用 g++ -shared -fPIC -O2 demo.cpp -o libdemo.so
// Windows 下需要 __declspec(dllexport),并编译为 DLL
#include <cstddef>
extern "C" int add(int a, int b) {
return a + b;
}
extern "C" std::size_t count_char(const char* text, char target) {
if (text == nullptr) return 0;
std::size_t n = 0;
for (const char* p = text; *p != '\0'; ++p) {
if (*p == target) ++n;
}
return n;
}Python 侧声明参数与返回类型,然后像普通函数一样调用:
# 适用于 Python 3.x
import ctypes
lib = ctypes.CDLL("./libdemo.so") # Windows 用 "demo.dll"
lib.add.argtypes = [ctypes.c_int, ctypes.c_int]
lib.add.restype = ctypes.c_int
lib.count_char.argtypes = [ctypes.c_char_p, ctypes.c_char]
lib.count_char.restype = ctypes.c_size_t
print(lib.add(2, 3))
print(lib.count_char(b"hello world", b"l"))关键点是必须设置argtypes和restype。不设置时ctypes默认把返回值当int处理,指针和 64 位整数会被截断,在 64 位平台上表现为莫名其妙的崩溃。另外,ctypes.CDLL在调用期间会释放 GIL,而ctypes.PyDLL不会,需要回调进 Python 时要注意这个区别。
再看标准 C 扩展。它必须包含Python.h,且该头文件要放在所有系统头之前;模块入口名必须与模块名严格对应:
// 适用于 CPython 3.x
#define PY_SSIZE_T_CLEAN
#include <Python.h>
static PyObject* py_add(PyObject* self, PyObject* args) {
long a = 0;
long b = 0;
if (!PyArg_ParseTuple(args, "ll", &a, &b)) {
return NULL; // 参数错误,异常已设置
}
return PyLong_FromLong(a + b);
}
static PyMethodDef demo_methods[] = {
{"add", py_add, METH_VARARGS, "返回两个整数之和"},
{NULL, NULL, 0, NULL}
};
static struct PyModuleDef demo_module = {
PyModuleDef_HEAD_INIT,
"demo",
"示例扩展模块",
-1,
demo_methods
};
PyMODINIT_FUNC PyInit_demo(void) {
return PyModule_Create(&demo_module);
}PyArg_ParseTuple的格式串"ll"表示两个long,失败时返回假并已设置异常,此时必须返回NULL让解释器继续传播。PyLong_FromLong返回的是新引用,调用者负责在合适的时候Py_DECREF;在模块函数里直接返回给解释器,所有权随之转移。
构建用setuptools的Extension:
# 适用于 Python 3.x + setuptools
from setuptools import setup, Extension
setup(
name="demo",
version="1.0",
ext_modules=[Extension("demo", ["demo.c"], language="c")],
)执行python setup.py build_ext --inplace会在当前目录生成可导入的模块。C++ 源文件把language改成"c++"即可,但模块入口仍要保证是 C 链接——PyMODINIT_FUNC已经在 C++ 下展开为extern "C",不要自己再套一层导致重复。
pybind11 的写法更贴近 C++,但它是第三方库,API 与支持的标准版本随版本变化,实际使用时以官方文档为准:
// 需要 C++17 与 pybind11;示例仅示意结构
#include <pybind11/pybind11.h>
int add(int a, int b) { return a + b; }
PYBIND11_MODULE(demo, m) {
m.doc() = "pybind11 示例";
m.def("add", &add, "两个整数相加");
}二、C++ 调 Python:嵌入解释器
嵌入的核心是「初始化 → 执行 → 清理」。Py_Initialize之后解释器才可用,结束时用Py_FinalizeEx释放资源。执行脚本可以用PyRun_SimpleString,调用具体函数则要走模块导入与属性查找。
// 适用于 C++17 + CPython 3.8 及以上
#include <Python.h>
#include <iostream>
#include <string>
int main() {
Py_Initialize();
if (!Py_IsInitialized()) {
std::cerr << "Python 初始化失败\n";
return 1;
}
// 把当前目录加入模块搜索路径,避免依赖环境变量
PyRun_SimpleString("import sys\nsys.path.insert(0, '.')");
PyObject* module = PyImport_ImportModule("math");
if (module == nullptr) {
PyErr_Print();
Py_FinalizeEx();
return 1;
}
PyObject* func = PyObject_GetAttrString(module, "sqrt");
if (func == nullptr || !PyCallable_Check(func)) {
PyErr_Print();
Py_XDECREF(func);
Py_DECREF(module);
Py_FinalizeEx();
return 1;
}
PyObject* result = PyObject_CallFunction(func, "d", 2.0);
if (result == nullptr) {
PyErr_Print();
} else {
double value = PyFloat_AsDouble(result);
if (PyErr_Occurred()) PyErr_Print();
else std::cout << "sqrt(2) = " << value << '\n';
Py_DECREF(result);
}
Py_DECREF(func);
Py_DECREF(module);
if (Py_FinalizeEx() < 0) {
std::cerr << "解释器清理失败\n";
return 1;
}
return 0;
}这段代码体现了 C API 的两条铁律。其一,每个返回新引用的函数都要配一次Py_DECREF:PyImport_ImportModule、PyObject_GetAttrString、PyObject_CallFunction都返回新引用,漏掉就是内存泄漏。其二,返回NULL表示 Python 异常已经挂起,必须调用PyErr_Print()或自行提取,不能在异常未清除的情况下继续调用 API。
如果 C++ 侧有多个线程要调用 Python,还需要 GIL(global interpreter lock,全局解释器锁)。从非 Python 创建的线程进入解释器时,用PyGILState_Ensure()和PyGILState_Release()包住这一段:
// 适用于 C++17 + CPython 3.x
void call_from_worker() {
PyGILState_STATE state = PyGILState_Ensure();
PyObject* builtins = PyEval_GetBuiltins(); // 借用引用,不要 DECREF
(void)builtins;
// ... 在这里调用 Python API ...
PyGILState_Release(state);
}PyEval_GetBuiltins返回借用引用(borrowed reference),对它调用Py_DECREF会破坏解释器状态。区分「新引用」与「借用引用」是写扩展时最容易出错的地方:规则不是「看到指针就释放」,而是看文档标注的所有权。
三、构建与部署要点
扩展模块和嵌入程序都需要让编译器找到 Python 头文件与库。用python3-config --includes --ldflags(Linux、macOS)可以打印出所需参数;Windows 下头文件在安装目录的include子目录,链接库是python3x.lib,运行时需要对应 DLL 在搜索路径中。用sysconfig模块也能拿到同样的信息:
# 适用于 Linux / macOS,仅用于查看编译参数
python3 -c "import sysconfig; print(sysconfig.get_paths()['include'])"
python3-config --includes --ldflags --embed注意--embed只对「嵌入 Python」的程序有意义;普通扩展模块不需要链接libpython,在 Linux 上通常也不应该链接,否则会出现符号重复。最后一个实践建议:C++ 异常绝不能穿过 C API 的栈帧。所有可能抛出的代码都要在边界内try/catch,再转成PyErr_SetString设置 Python 异常并返回NULL。反向也一样,Python 回调 C++ 时要把异常挡在回调内部。
常见坑点
坑点 1:Python.h不是第一个被包含的头。
❌ 先#include <vector>再#include <Python.h>,某些平台上_POSIX_C_SOURCE等宏已被定义,导致编译警告或结构体定义冲突。
✅ 把Python.h放在文件最前面,并在其前定义PY_SSIZE_T_CLEAN。
坑点 2:ctypes不声明argtypes。
❌lib.add(2, 3)直接调用,64 位返回值被按int截断。
✅ 设置argtypes与restype,指针用c_void_p、c_char_p,大小用c_size_t。
坑点 3:模块入口名与文件名不一致。
❌ 文件叫mymodule.c,入口写成PyInit_demo,import mymodule报ImportError: dynamic module does not define module export function。
✅ 入口必须是PyInit_加模块名,且与Extension的第一个参数一致。
坑点 4:忘记Py_DECREF。
❌ 只PyObject_GetAttrString不释放,长时间运行后内存持续增长。
✅ 成对使用Py_DECREF;不确定是否为NULL时用Py_XDECREF。
坑点 5:对借用引用调用Py_DECREF。
❌ 对PyEval_GetBuiltins、PyTuple_GetItem等返回的借用引用减引用,造成提前释放。
✅ 查文档确认所有权;需要长期保存借用引用时先Py_INCREF。
坑点 6:C++ 异常穿透 C ABI。
❌ 扩展函数里抛出std::runtime_error,进程直接终止。
✅ 在边界内捕获所有异常,用PyErr_SetString(PyExc_RuntimeError, e.what())转换后返回NULL。
坑点 7:多线程调用 Python 不加 GIL。
❌ 工作线程直接调用PyObject_Call,数据竞争导致解释器崩溃。
✅ 用PyGILState_Ensure/PyGILState_Release包住调用;ctypes.CDLL调用期间已释放 GIL,回调进 Python 时要重新获取。
坑点 8:把嵌入程序和扩展模块的链接方式搞混。
❌ 给扩展模块加--embed,或嵌入程序忘了链接libpython。
✅ 扩展模块只包含头文件;嵌入程序按python3-config --embed --ldflags链接。
总结
| 方向 | 机制 | 关键规则 |
|---|
| Python 调 C | ctypes | 声明argtypes/restype,只传 C 兼容类型 |
| Python 调 C++ | CPython 扩展 | extern "C"入口,参数解析失败返回NULL |
| Python 调 C++ | pybind11 | 依赖第三方库,版本以官方文档为准 |
| C++ 调 Python | 嵌入解释器 | 初始化、错误打印、Py_FinalizeEx三步齐全 |
| 跨线程 | GIL | 非 Python 线程进入前PyGILState_Ensure |
两个方向的共同点是「边界要窄」:只暴露 C ABI 能表达的类型,把异常和内存管理都在边界处处理干净。把ctypes当作快速验证工具,把 C 扩展或 pybind11 当作长期维护的接口,把嵌入解释器限制在明确的脚本入口上,三者各司其职,比混用要可靠得多。