You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.26 22:33:22