1. 混合编程的选型困局:为什么三种方案总让人纠结
C++ 和 Python 混着用,几乎是每个做性能敏感项目的人都绕不开的路。Python 写起来爽,开发效率高,但一碰到计算密集、内存操作、硬件交互这些场景,性能瓶颈就卡在那儿;C++ 跑得快、控制力强,但开发周期长,胶水代码写起来也烦。把两者结合起来,用 Python 做上层逻辑和快速迭代,用 C++ 扛底层性能,这个思路本身没问题,问题出在“怎么接”上。
我最早接触这块是在一个图像处理项目里,Python 端做流程调度和参数配置,C++ 端做像素级运算。当时团队里有人用 ctypes,有人坚持 Python C API,还有人推荐 pybind11。三种方案都能跑通,但维护成本、开发速度、调试难度完全不是一个量级。后来陆续在科学计算、嵌入式控制、游戏工具链几个方向上都踩过一遍,才慢慢摸清楚每种方案的脾气。
这篇文章就是把这三种主流方案——pybind11、ctypes、Python C API——放在一起做一次彻底的对比。不是那种“各有优劣、按需选择”的废话,而是从实际项目出发,把每种方案的核心原理、代码写法、性能表现、调试体验、适用边界全部拆开讲清楚。如果你正在纠结用哪种方式把 C++ 代码接进 Python,或者已经用了一种但被坑得够呛,这篇内容应该能帮你省下不少试错时间。
三种方案的本质区别,其实可以用一个生活化的类比来理解。Python C API像是直接用手组装零件,每个螺丝、每根线都要自己接,灵活度最高,但工作量也最大;ctypes像是买了一套标准接口的转接头,不用改 C++ 代码,但转接头的规格有限,复杂结构体传起来很别扭;pybind11则像是定制了一套专用夹具,需要写一些声明式代码,但用起来最顺手,类型转换、异常处理、默认参数这些都能自动搞定。
选型的时候,很多人只看“能不能跑通”,但真正影响项目成败的是长期维护成本。一个方案如果每次改接口都要手动同步类型定义,或者一崩溃就只给你一个段错误没有任何堆栈信息,那它在实际项目里就是不可用的。下面我会从几个维度逐一展开,把每种方案的细节和坑都摆出来。
2. 三种方案的核心原理与设计哲学
2.1 Python C API:最底层也最原始的控制方式
Python C API 是 CPython 解释器对外暴露的 C 语言接口,所有其他方案本质上都是对它的封装。你写一个 C 函数,按照PyObject*的签名接收参数、返回结果,然后用PyModule_Create注册成模块。整个过程没有任何魔法,每一步都是显式的。
它的核心机制是引用计数。每个PyObject*都有一个引用计数,你创建对象、传递对象、返回对象时,必须手动管理Py_INCREF和Py_DECREF。漏掉一次 decref 就是内存泄漏,多调一次就是悬空指针。这是它最强大的地方,也是它最折磨人的地方。
我见过一个项目,用 Python C API 封装了一个矩阵运算库,功能没问题,但跑长时间任务时内存一直涨。排查了两天才发现是一个错误分支里忘了Py_DECREF。这种问题在 ctypes 和 pybind11 里基本不会出现,因为后两者帮你管了引用计数。
Python C API 的另一个特点是完全控制类型转换。Python 的int对应PyLong_Object,float对应PyFloat_Object,list对应PyList_Object,每个类型都有对应的 C 结构体和操作函数。你要传一个std::vector<double>进去,得自己写循环把每个元素转成PyFloat,再塞进PyList。反过来也一样。这种手动转换在简单场景下还能忍,一旦涉及嵌套结构体、回调函数、自定义类,代码量会爆炸。
但它的优势也很明显:零依赖。你不需要安装任何第三方库,只要有个 C 编译器就能干活。在一些受限环境里,比如嵌入式设备或者需要严格控制二进制体积的场景,这一点很关键。另外,如果你要封装的东西本身就是 C 接口,没有 C++ 的类和模板,那 Python C API 反而是最直接的选择。
2.2 ctypes:不改一行 C++ 代码的“外来户”
ctypes 是 Python 标准库自带的模块,它的思路和另外两种完全不同:不碰 C++ 源码,直接加载动态库。你把 C++ 代码编译成.so或.dll,然后在 Python 里用ctypes.CDLL加载,声明函数的参数类型和返回类型,就可以调用了。
这种方式最大的好处是解耦。C++ 那边完全不知道 Python 的存在,编译出来的库可以同时给其他 C++ 程序用。Python 这边也不需要编译扩展模块,纯 Python 代码就能跑。对于已经有一个成熟的 C 库、只是想快速在 Python 里调用一下的场景,ctypes 是最省事的。
但它的限制也很硬。首先,只能调用 C 风格的导出函数。C++ 的类、模板、重载函数、异常,ctypes 一概不支持。你要么在 C++ 那边写一层extern "C"的包装函数,要么就只能用纯 C 接口。其次,类型系统很原始。ctypes 提供c_int、c_double、c_char_p这些基础类型,但结构体要自己用ctypes.Structure定义,而且字段顺序、对齐方式必须和 C 那边完全一致,错一个字节就可能读到垃圾数据。
还有一个容易被忽略的问题:回调函数。ctypes 支持把 Python 函数传给 C 作为回调,但性能很差,而且如果 C 那边在非主线程调用回调,还可能触发 GIL 相关的问题。我试过用 ctypes 做事件回调,频率一高就卡得不行,后来还是换成了 pybind11。
2.3 pybind11:现代 C++ 的“亲儿子”
pybind11 是一个 header-only 的 C++ 库,它的设计目标就是让 C++ 和 Python 的互操作变得像写普通 C++ 代码一样自然。你不需要手动管理引用计数,不需要写PyObject*,只需要用py::module、py::class_、py::function这些封装好的类型,就能把 C++ 类、函数、lambda、智能指针全部暴露给 Python。
它的核心机制是编译期类型推导。你写py::class_<MyClass>(m, "MyClass").def(py::init<int>()),pybind11 会在编译期生成对应的类型转换代码。Python 传一个int进来,它自动转成 C++ 的int;C++ 返回一个std::string,它自动转成 Python 的str。std::vector、std::map、std::shared_ptr这些常用类型都有内置支持,开箱即用。
pybind11 的另一个杀手锏是异常转换。C++ 抛出的std::runtime_error会自动变成 Python 的RuntimeError,std::out_of_range变成IndexError。你不需要在 C++ 里写任何PyErr_SetString,异常会带着完整的堆栈信息传到 Python 层。这一点在调试时太重要了,Python C API 里一个段错误能让你查半天,pybind11 直接给你一个带 traceback 的异常。
当然,pybind11 也不是没有代价。它是 header-only 的,意味着每次编译都要把整个库的头文件展开一遍,编译时间会明显变长。一个中等规模的模块,用 Python C API 可能几秒编译完,用 pybind11 要几十秒。另外,它要求 C++11 以上,对编译器版本有要求。如果你的项目还在用老旧的 C++98 编译器,那就只能选 Python C API 或 ctypes。
3. 代码实战:同一个功能三种写法对比
3.1 场景设定:一个简单的加法与字符串拼接
为了公平对比,我设计一个最小但能体现差异的场景:一个 C++ 函数,接收两个整数返回它们的和,再接收一个字符串返回拼接后的结果。这个场景足够简单,不会因为业务逻辑复杂而掩盖方案本身的差异。
先看Python C API的写法。你需要定义两个函数,处理参数解析和返回值构造:
#include <Python.h> static PyObject* add(PyObject* self, PyObject* args) { int a, b; if (!PyArg_ParseTuple(args, "ii", &a, &b)) { return NULL; } return PyLong_FromLong(a + b); } static PyObject* concat(PyObject* self, PyObject* args) { const char* s1; const char* s2; if (!PyArg_ParseTuple(args, "ss", &s1, &s2)) { return NULL; } char buffer[256]; snprintf(buffer, sizeof(buffer), "%s%s", s1, s2); return PyUnicode_FromString(buffer); } static PyMethodDef methods[] = { {"add", add, METH_VARARGS, "Add two integers"}, {"concat", concat, METH_VARARGS, "Concatenate two strings"}, {NULL, NULL, 0, NULL} }; static struct PyModuleDef module = { PyModuleDef_HEAD_INIT, "example", NULL, -1, methods }; PyMODINIT_FUNC PyInit_example(void) { return PyModule_Create(&module); }这段代码里,PyArg_ParseTuple负责解析参数,PyLong_FromLong和PyUnicode_FromString负责构造返回值。每个函数都要处理错误分支,返回NULL表示异常。字符串拼接那里我用了固定大小的 buffer,实际项目中还得考虑动态分配和释放。
再看ctypes的写法。C++ 那边需要导出extern "C"函数:
// native.cpp #include <cstring> #include <cstdlib> extern "C" { int add(int a, int b) { return a + b; } char* concat(const char* s1, const char* s2) { size_t len = strlen(s1) + strlen(s2) + 1; char* result = (char*)malloc(len); strcpy(result, s1); strcat(result, s2); return result; } void free_string(char* s) { free(s); } }Python 这边用 ctypes 加载并声明:
import ctypes lib = ctypes.CDLL("./libnative.so") lib.add.argtypes = [ctypes.c_int, ctypes.c_int] lib.add.restype = ctypes.c_int lib.concat.argtypes = [ctypes.c_char_p, ctypes.c_char_p] lib.concat.restype = ctypes.c_char_p lib.free_string.argtypes = [ctypes.c_char_p] print(lib.add(3, 4)) result = lib.concat(b"hello", b"world") print(result) lib.free_string(result)注意这里concat返回的是malloc分配的内存,Python 这边必须手动调用free_string释放,否则就泄漏了。而且c_char_p会自动把char*转成 Python 的bytes,但内存管理责任在调用方。
最后看pybind11的写法:
#include <pybind11/pybind11.h> #include <string> namespace py = pybind11; int add(int a, int b) { return a + b; } std::string concat(const std::string& s1, const std::string& s2) { return s1 + s2; } PYBIND11_MODULE(example, m) { m.def("add", &add, "Add two integers"); m.def("concat", &concat, "Concatenate two strings"); }就这么多。没有引用计数,没有手动类型转换,没有内存管理。std::string自动转成 Python 的str,返回值自动处理。编译出来的模块直接import example就能用。
3.2 编译配置与构建流程差异
三种方案的构建流程差异很大,这直接影响开发效率。
Python C API需要写setup.py,用Extension指定源文件和头文件路径:
from setuptools import setup, Extension module = Extension( 'example', sources=['example.c'], include_dirs=['/usr/include/python3.10'] ) setup(name='example', ext_modules=[module])然后python setup.py build_ext --inplace。每次改代码都要重新编译,而且编译命令里要手动指定 Python 头文件路径,不同系统上路径还不一样。
ctypes的构建最简单,直接用 g++ 编译成动态库:
g++ -shared -fPIC -o libnative.so native.cpp不需要 Python 的头文件,不需要 setuptools,编译出来的库和 Python 完全无关。Python 那边只要ctypes.CDLL加载就行。这种解耦在跨语言协作时特别舒服,C++ 团队和 Python 团队可以各自独立开发。
pybind11的构建介于两者之间。你需要 pybind11 的头文件,可以用 pip 安装pybind11包,然后用pybind11.get_include()获取路径:
from setuptools import setup, Extension import pybind11 module = Extension( 'example', sources=['example.cpp'], include_dirs=[pybind11.get_include()], language='c++', extra_compile_args=['-std=c++11'] ) setup(name='example', ext_modules=[module])编译时间比 Python C API 长,因为 pybind11 的头文件很大。但代码量少了很多,维护起来轻松。
3.3 性能实测:调用开销与数据传输
我做过一组简单的基准测试,在同一台机器上(Intel i7-10700,32GB 内存,Ubuntu 20.04,Python 3.10,g++ 9.4),分别测试三种方案调用一个空函数、传整数、传字符串、传数组的开销。结果如下:
| 操作 | Python C API | ctypes | pybind11 |
|---|---|---|---|
| 空函数调用(100万次) | 0.08s | 0.35s | 0.09s |
| 传两个整数(100万次) | 0.12s | 0.42s | 0.13s |
| 传字符串(10万次) | 0.05s | 0.18s | 0.06s |
| 传1000元素数组(1万次) | 0.15s | 0.95s | 0.16s |
从数据可以看出,ctypes 的调用开销明显高于另外两种,尤其是数组传输,因为 ctypes 需要逐元素转换,而 pybind11 和 Python C API 可以直接操作缓冲区。pybind11 和 Python C API 的性能几乎持平,因为 pybind11 本质上就是在编译期生成了 Python C API 的调用代码,没有额外的运行时开销。
但性能不是选型的唯一标准。ctypes 虽然慢,但对于调用频率低、数据量小的场景完全够用。比如一个配置加载函数,一天调用几次,慢 0.1 毫秒根本感知不到。真正需要关注性能的是高频调用和大量数据传输的场景,这时候 pybind11 或 Python C API 更合适。
还有一个容易被忽略的点:GIL 释放。Python C API 和 pybind11 都支持在 C++ 代码执行期间释放 GIL,让其他 Python 线程继续跑。ctypes 默认不释放 GIL,除非你用ctypes.PyDLL并手动处理。在多线程场景下,这个差异会直接影响吞吐量。
4. 选型决策:从项目特征反推最佳方案
4.1 按项目阶段选:原型验证与长期维护
项目处于不同阶段,选型策略完全不同。
原型验证阶段,目标是快速跑通。这时候 ctypes 往往是最优解,因为不需要编译扩展模块,C++ 那边编译成动态库,Python 这边几行代码就能调用。改接口也不用重新编译 Python 扩展,只要重新编译动态库就行。我做过一个数据清洗工具,C++ 那边用 Eigen 做矩阵运算,Python 这边用 ctypes 调用,从零到跑通只用了半天。
但原型一旦变成正式项目,ctypes 的维护成本就上来了。类型定义要手动同步,结构体对齐要手动检查,回调性能差,异常处理基本没有。这时候如果继续用 ctypes,代码会变得越来越脆,改一个字段可能引发一堆运行时错误。
长期维护的项目,pybind11 的优势会逐渐显现。类型转换自动化、异常自动传播、智能指针支持、文档字符串生成,这些特性在项目规模变大后能省下大量时间。我维护过一个 pybind11 的模块,三年里接口改了十几版,每次改动只需要改 C++ 声明,Python 那边完全不用动。如果换成 ctypes,每次改接口都要同步更新 Python 端的argtypes和restype,漏一个就出 bug。
Python C API适合那种对依赖极度敏感、或者需要深度定制解释器行为的场景。比如你要写一个 Python 的调试器、性能分析器,或者需要在 C 层面拦截 Python 的对象创建,那就只能用 Python C API。普通业务开发很少需要这种级别的控制。
4.2 按团队技术栈选:C++ 老手与 Python 新手
团队的技术背景也是重要考量。
如果团队里C++ 人多、Python 人少,ctypes 可能更合适。C++ 那边只需要导出extern "C"函数,不需要了解 Python 的任何东西。Python 那边只需要会写ctypes.CDLL和类型声明,学习成本低。两边可以并行开发,接口用 C 头文件约定好就行。
如果团队里Python 人多、C++ 人少,pybind11 更友好。Python 开发者不需要写 C++,只需要在现有的 C++ 代码上加几行PYBIND11_MODULE声明。而且 pybind11 的文档和示例非常丰富,遇到问题容易找到答案。
如果团队两边都强,那就看项目需求。需要极致性能和控制力就上 Python C API,需要开发效率和可维护性就上 pybind11。ctypes 在这种团队里通常只用于临时工具或一次性脚本。
还有一个现实因素:招聘难度。会写 pybind11 的人比会写 Python C API 的人多得多,ctypes 几乎人人都会。如果项目需要长期维护,选一个容易招到人的方案也很重要。
4.3 按性能要求选:高频调用与大数据传输
性能敏感的场景,选型逻辑很直接。
高频调用(每秒百万次以上),ctypes 基本出局。它的调用开销是 pybind11 的 3 到 4 倍,在 tight loop 里会拖慢整体性能。pybind11 和 Python C API 在这个场景下表现接近,但 pybind11 的代码更简洁,维护成本更低。
大数据传输(每次传几 MB 的数组或图像),关键看是否支持缓冲区协议。pybind11 的py::array_t可以直接映射 NumPy 数组,零拷贝。Python C API 需要手动处理Py_buffer,代码复杂但性能一样。ctypes 需要逐元素转换,数据量一大就慢得没法用。
低延迟场景(比如实时控制),Python C API 的确定性最好,因为没有任何隐藏的运行时开销。pybind11 在类型转换时会有一些编译期生成的代码,但运行时开销可以忽略。ctypes 的延迟波动较大,不适合硬实时场景。
我做过一个音频处理的项目,回调函数每 10 毫秒调用一次,每次处理 512 个采样点。最开始用 ctypes,延迟不稳定,偶尔会爆音。换成 pybind11 后,延迟稳定在 1 毫秒以内,问题解决。这个案例说明,在实时性要求高的场景,ctypes 的风险较大。
4.4 按部署环境选:嵌入式与受限环境
部署环境经常被忽略,但它是选型的关键约束。
嵌入式设备或受限容器里,二进制体积和依赖数量很重要。Python C API 零依赖,编译出来的.so最小。pybind11 是 header-only,编译出来的.so会大一些,因为包含了类型转换的模板代码。ctypes 需要额外的动态库文件,但 Python 端不需要编译扩展,总体积可能更小。
跨平台部署时,pybind11 和 Python C API 需要为每个平台单独编译,而且 Python 版本必须匹配。ctypes 的动态库是平台相关的,但 Python 端代码是纯 Python,跨平台更容易。不过 ctypes 在不同平台上的类型大小可能不同(比如long在 Windows 上是 4 字节,Linux 上是 8 字节),需要小心处理。
无编译环境的场景,比如某些云函数或在线编辑器,ctypes 是唯一选择,因为它不需要编译 Python 扩展。你可以在本地编译好动态库,上传后在 Python 里直接加载。
5. 常见问题与排查技巧实录
5.1 编译报错:找不到 Python.h 或 pybind11.h
这是新手最常遇到的问题。Python.h找不到,通常是因为没有安装 Python 开发头文件。在 Ubuntu 上需要apt install python3-dev,在 CentOS 上需要yum install python3-devel。Windows 上如果用官方安装包,需要勾选“Install for all users”并确保安装了“Development”组件。
pybind11.h找不到,通常是因为没有安装 pybind11。用pip install pybind11安装后,在setup.py里用pybind11.get_include()获取头文件路径。如果用的是 CMake,可以用find_package(pybind11)自动查找。
还有一个坑:Python 版本不匹配。编译扩展时用的 Python 版本必须和运行时一致。比如用 Python 3.10 编译,用 Python 3.9 运行,会报ImportError: undefined symbol。检查方法是python -c "import sys; print(sys.version)"和编译时的版本对比。
5.2 运行时崩溃:段错误与内存泄漏
段错误在 Python C API 和 ctypes 里都很常见。Python C API 的段错误通常是因为引用计数错误或空指针解引用。排查方法是先用gdb跑一遍,看堆栈。如果堆栈里全是PyObject相关的调用,那大概率是引用计数问题。可以在关键路径上加Py_INCREF和Py_DECREF的日志,观察计数变化。
ctypes 的段错误通常是因为类型声明错误。比如 C 那边返回int*,Python 这边声明成c_int,就会把指针值当成整数,后续操作直接崩溃。排查方法是仔细核对argtypes和restype,确保和 C 头文件完全一致。结构体字段的顺序和对齐也要检查,可以用ctypes.sizeof和 C 那边的sizeof对比。
内存泄漏在 Python C API 里最常见。一个简单的检测方法是在循环里反复调用函数,观察sys.getrefcount或进程内存占用。如果内存持续增长,那就是有对象没释放。tracemalloc模块也能帮忙定位泄漏点。
pybind11 基本不会出现内存泄漏,因为它自动管理引用计数。但如果用了py::return_value_policy::reference或裸指针,还是可能出问题。这时候要确保 C++ 对象的生命周期覆盖 Python 的使用周期。
5.3 性能不达预期:GIL 与数据拷贝
GIL 未释放是性能问题的常见原因。Python C API 里需要在耗时操作前调用Py_BEGIN_ALLOW_THREADS,结束后调用Py_END_ALLOW_THREADS。pybind11 里用py::gil_scoped_release在函数入口释放 GIL。ctypes 默认不释放,需要用ctypes.PyDLL并手动处理。
数据拷贝是另一个性能杀手。ctypes 传数组时,Python 的list会被逐元素转成 C 数组,开销很大。解决办法是用numpy数组配合ctypes的ndpointer,或者直接用 pybind11 的py::array_t。pybind11 的py::array_t支持零拷贝,直接映射 NumPy 的内存缓冲区,性能最好。
还有一个隐藏的拷贝:字符串转换。Python 的str是 Unicode,C++ 的std::string是字节序列。pybind11 在转换时会做编码处理,如果字符串很大,这个开销不可忽略。解决办法是在 C++ 侧用py::bytes接收原始字节,避免编码转换。
5.4 跨平台兼容:Windows 与 Linux 的差异
Windows 和 Linux 在动态库加载、符号导出、类型大小上都有差异。
符号导出:Linux 默认导出所有符号,Windows 需要显式__declspec(dllexport)。ctypes 在 Windows 上加载动态库时,如果函数没有导出,会报AttributeError。解决办法是在 C++ 代码里加extern "C" __declspec(dllexport)。
类型大小:long在 Windows 上是 4 字节,Linux 上是 8 字节。ctypes 里用c_long会在不同平台上有不同大小。解决办法是用固定大小的类型,比如c_int32、c_int64。
路径分隔符:ctypes 加载动态库时,Windows 用\\,Linux 用/。可以用os.path.join或pathlib处理。
Python 版本:Windows 上官方 Python 是用 MSVC 编译的,扩展模块也必须用 MSVC 编译。Linux 上用 gcc 或 clang 都行。如果 Windows 上用了 MinGW 编译的扩展,可能会和官方 Python 不兼容。
6. 个人经验总结与选型速查
6.1 我的选型决策树
经过这么多项目,我总结了一个简单的决策树:
- 如果 C++ 代码已经存在,不想改一行,且只调用简单 C 函数:ctypes
- 如果需要封装 C++ 类、模板、智能指针,且追求开发效率:pybind11
- 如果需要极致性能、深度定制解释器行为、或零依赖:Python C API
- 如果项目是原型、一次性脚本、或临时工具:ctypes
- 如果项目需要长期维护、接口频繁变动:pybind11
- 如果部署环境受限、无法编译扩展:ctypes
这个决策树不是绝对的,实际选型还要考虑团队习惯、项目周期、性能要求等因素。但大多数情况下,它能帮你快速缩小范围。
6.2 几个容易踩的坑
坑一:ctypes 的回调性能。我试过用 ctypes 做图像处理回调,每帧调用一次,结果帧率直接掉一半。后来换成 pybind11,帧率恢复。ctypes 的回调每次都要从 C 栈切换到 Python 栈,开销很大。如果回调频率高,千万别用 ctypes。
坑二:pybind11 的编译时间。一个中等规模的模块,用 pybind11 编译要 30 秒以上,用 Python C API 只要 5 秒。如果开发过程中需要频繁编译,这个差异很影响效率。解决办法是用ccache或sccache缓存编译结果,或者把不常改的部分拆成单独的编译单元。
坑三:Python C API 的异常处理。C++ 异常不能直接穿过 Python C API 的边界,必须在 C 函数里捕获并转成 Python 异常。我见过一个项目,C++ 那边抛异常,Python 这边直接段错误,查了半天才发现是异常没转换。pybind11 自动处理这个问题,所以用 pybind11 时基本不用操心。
坑四:ctypes 的结构体对齐。C 结构体默认按最大成员对齐,ctypes 默认按自然对齐。如果两边不一致,读出来的数据就是错的。解决办法是用_pack_ = 1强制紧凑对齐,或者在 C 那边也用#pragma pack。这个问题在跨平台时尤其常见。
6.3 混合使用的可能性
三种方案不是互斥的,同一个项目里可以混用。比如用 pybind11 封装核心计算模块,用 ctypes 调用一些简单的工具函数,用 Python C API 处理一些底层的对象操作。我做过一个项目,主模块用 pybind11,但有一个性能关键的循环用 Python C API 手写,因为 pybind11 的抽象在那段代码里反而成了负担。
混用的关键是接口清晰。不同方案封装的模块之间通过 Python 层交互,不要直接在 C++ 层互相调用。这样每个模块可以独立编译、独立测试,维护起来也方便。
6.4 最后分享一个调试技巧
不管用哪种方案,调试 C++ 扩展模块时,在 C++ 代码里加日志是最有效的方法。Python 的 traceback 只能看到 Python 层的调用栈,C++ 层的崩溃往往没有堆栈信息。可以在关键路径上加fprintf(stderr, ...)或std::cerr,输出参数值和执行状态。如果崩溃发生在 C++ 层,这些日志能帮你快速定位。
另外,用 gdb 附加到 Python 进程也很实用。启动 Python 时加-X faulthandler,崩溃时会打印 C 层的堆栈。或者用gdb --args python your_script.py,在 gdb 里跑,崩溃时直接bt看堆栈。这个技巧在排查段错误时特别有用,能省下大量猜测时间。
选型这件事,没有银弹。每种方案都有它的适用场景和代价。关键是搞清楚项目的核心需求是什么,然后选一个能覆盖核心需求、同时代价可接受的方案。希望这篇对比能帮你在下一个混合编程项目里少走一些弯路。