C/C++嵌入Python第二次调用PyObject_CallObject触发Bad access崩溃如何解决
C/C++嵌入Python重复调用触发BAD_ACCESS崩溃问题排查
问题背景
相关技术领域初学者首次尝试在C/C++代码中调用存储在程序运行目录外的Python文件函数,自定义实现的ReadPythonFile函数首次调用无异常,第二次调用就触发BAD_ACCESS内存访问崩溃,原问题代码如下:
void ReadPythonFile( float* input, float min , float max) { PyObject* py_imp_str = NULL; PyObject* py_imp_handle = NULL; PyObject* py_imp_dict = NULL; PyObject* py_imp_load_source = NULL; PyObject* py_dir = NULL; PyObject* py_lib_name = NULL; PyObject* py_args_tuple = NULL; PyObject* py_lib_mod = NULL; PyObject* py_func = NULL; //PyObject* py_ret = NULL; PyObject* py_lib_mod_dict = NULL; Py_Initialize(); PyGILState_STATE state = PyGILState_Ensure(); char argv1[] = "/SomeDir/myPythonTestFile.py" ; py_dir = PyUnicode_FromString(argv1); py_imp_str = PyString_FromString("imp"); py_imp_handle = PyImport_Import(py_imp_str); //normal python import for imp py_imp_dict = PyModule_GetDict(py_imp_handle); //borrowed Py_DECREF(py_imp_handle); py_imp_load_source = PyDict_GetItemString(py_imp_dict, "load_source"); Py_DECREF(py_imp_dict); py_lib_name = PyUnicode_FromString("test"); //setup args for imp.load_source py_args_tuple = PyTuple_New(2); PyTuple_SetItem(py_args_tuple, 0, py_lib_name); PyTuple_SetItem(py_args_tuple, 1, py_dir); //call imp.load_source py_lib_mod = PyObject_CallObject(py_imp_load_source, py_args_tuple); Py_DECREF(py_args_tuple); if (py_lib_mod) { py_lib_mod_dict = PyModule_GetDict(py_lib_mod); py_func = PyDict_GetItem(py_lib_mod_dict, py_lib_name); } else { PyErr_Print(); return; } Py_DECREF(py_lib_mod); PyGILState_Release(state); Py_Finalize(); return ; }
崩溃核心诱因
- 错误的解释器生命周期管理:每次调用函数都执行
Py_Initialize()和Py_Finalize()启停Python解释器。CPython对重复初始化-终结流程的兼容性很差,第一次调用Py_Finalize()会销毁所有Python内部状态、内存、模块缓存,残留的悬垂指针会在第二次调用时触发非法内存访问。 - 引用计数操作完全不符合规则:
PyModule_GetDict、PyDict_GetItemString、PyDict_GetItem返回的都是借用引用,内存由所属对象托管,不需要手动调用Py_DECREF。代码中对py_imp_dict调用DECREF前,所属的py_imp_handle已经提前被DECREF,可能已经被GC回收,py_imp_dict直接成为野指针。- 拿到
py_lib_mod后提前调用Py_DECREF,但后续获取的py_func是模块内的借用引用,模块被回收后py_func也成为野指针。 py_dir、py_imp_str、py_lib_name等通过Py*_FromString获取的新引用对象全程没有做DECREF,第一次调用就存在内存泄漏,错误分支直接return时泄漏更严重。
- GIL操作顺序错误:代码在
Py_Initialize()执行完才调用PyGILState_Ensure()获取GIL,且在释放GIL之后才执行Py_Finalize(),所有Python C API调用必须在持有GIL的状态下执行,顺序错误会触发未定义行为。 - 重复加载模块无缓存:每次调用都重新加载同一个Python文件,重复导入过程中容易和sys.modules中已缓存的模块对象产生引用冲突。另外代码中获取函数时传入的key是模块名
test,而非实际函数名,本身也存在逻辑错误。
修复方案
- 全局只初始化/终结一次Python解释器:将
Py_Initialize()放到程序启动阶段执行,Py_Finalize()放到程序退出前执行,禁止单次业务调用就启停解释器。 - 严格遵守Python C API引用计数规则:
- 新引用(接口返回值需要调用者负责释放的,比如
PyUnicode_FromString、PyImport_Import、PyObject_CallObject)用完必须调用Py_DECREF。 - 借用引用(接口返回值由其他对象托管的,比如
PyModule_GetDict、PyDict_GetItem*)绝对不要主动调用Py_DECREF,如果需要长期持有可以手动Py_INCREF增加引用计数。
- 新引用(接口返回值需要调用者负责释放的,比如
- 调整GIL操作顺序:所有Python C API调用必须在持有GIL的状态下执行,
Py_Initialize和Py_Finalize执行时也要保证持有GIL。 - 增加模块缓存:第一次加载Python模块和目标函数后,将对象存为静态全局变量,后续调用直接使用,避免重复导入开销。
修复后参考代码
// 全局缓存解释器、函数对象,只初始化一次 static PyObject* g_py_func = NULL; static bool g_py_inited = false; // 程序启动时调用一次初始化 bool InitPythonEnv() { Py_Initialize(); PyGILState_STATE state = PyGILState_Ensure(); PyObject* py_imp_str = NULL; PyObject* py_imp_handle = NULL; PyObject* py_imp_dict = NULL; PyObject* py_imp_load_source = NULL; PyObject* py_dir = NULL; PyObject* py_lib_name = NULL; PyObject* py_args_tuple = NULL; PyObject* py_lib_mod = NULL; PyObject* py_lib_mod_dict = NULL; bool ret = false; do { char argv1[] = "/SomeDir/myPythonTestFile.py" ; py_dir = PyUnicode_FromString(argv1); py_imp_str = PyString_FromString("imp"); py_imp_handle = PyImport_Import(py_imp_str); if (!py_imp_handle) break; py_imp_dict = PyModule_GetDict(py_imp_handle); // 借用引用,无需DECREF py_imp_load_source = PyDict_GetItemString(py_imp_dict, "load_source"); // 借用引用,无需DECREF if (!py_imp_load_source) break; py_lib_name = PyUnicode_FromString("test"); py_args_tuple = PyTuple_New(2); // PyTuple_SetItem会接管传入对象的引用,无需单独给py_lib_name、py_dir做DECREF PyTuple_SetItem(py_args_tuple, 0, py_lib_name); PyTuple_SetItem(py_args_tuple, 1, py_dir); py_lib_mod = PyObject_CallObject(py_imp_load_source, py_args_tuple); if (!py_lib_mod) { PyErr_Print(); break; } py_lib_mod_dict = PyModule_GetDict(py_lib_mod); // 借用引用,无需DECREF g_py_func = PyDict_GetItemString(py_lib_mod_dict, "target_func_name"); // 替换为Python侧实际函数名 if (!g_py_func) { PyErr_Print(); break; } Py_INCREF(g_py_func); // 长期持有函数对象,手动增加引用计数 ret = true; } while(0); // 统一清理所有新引用对象 Py_XDECREF(py_imp_str); Py_XDECREF(py_imp_handle); Py_XDECREF(py_args_tuple); Py_XDECREF(py_lib_mod); PyGILState_Release(state); g_py_inited = ret; return ret; } void ReadPythonFile(float* input, float min_val, float max_val) { if (!g_py_inited || !g_py_func) return; PyGILState_STATE state = PyGILState_Ensure(); // 此处组装参数调用g_py_func即可,需根据Python侧参数类型做对应转换 // 示例: // PyObject* args = PyTuple_New(3); // PyTuple_SetItem(args, 0, PyCapsule_New(input, NULL, NULL)); // 按实际参数类型封装 // PyTuple_SetItem(args, 1, PyFloat_FromDouble(min_val)); // PyTuple_SetItem(args, 2, PyFloat_FromDouble(max_val)); // PyObject* ret = PyObject_CallObject(g_py_func, args); // Py_XDECREF(args); // Py_XDECREF(ret); PyGILState_Release(state); } // 程序退出前调用一次清理 void ReleasePythonEnv() { if (!g_py_inited) return; PyGILState_STATE state = PyGILState_Ensure(); Py_XDECREF(g_py_func); PyGILState_Release(state); Py_Finalize(); g_py_inited = false; }
注意:如果Python侧接收numpy数组类型参数,需要提前初始化numpy C API,不要直接将C裸指针传入Python层。
内容的提问来源于stack exchange,提问作者Swati Kala
相关产品推荐
相关产品推荐

