C++ DLL返回PyObject函数用ctypes调用报访问违例如何解决
问题根因
你碰到的内存访问错误核心来自两点:
- Python所有C API调用都要求调用方持有全局解释器锁(GIL),你在DLL中直接调用
PyList_New时没有提前获取GIL,导致访问Python内部内存空间时报错。 - 潜在问题:编译DLL时依赖的Python头文件、库文件的版本、架构必须和你运行测试脚本的Python环境完全匹配,比如用64位Python3.9运行就要对应链接64位Python3.9的lib文件,否则也会出现调用异常。
现有代码修复方案
修改C++代码,在调用Python API前后加GIL的获取和释放逻辑即可:
#include <Python.h> #ifdef _MSC_VER #define DLL_EXPORT __declspec( dllexport ) #else #define DLL_EXPORT #endif #ifdef __cplusplus extern "C"{ #endif DLL_EXPORT PyObject *test3() { // 提前获取GIL PyGILState_STATE gstate = PyGILState_Ensure(); PyObject* ptr = PyList_New(10); // 释放GIL,返回的PyObject引用计数正常,Python侧会自动接管 PyGILState_Release(gstate); return ptr; } #ifdef __cplusplus } #endif
现有Python测试代码不需要修改,只需确认LIB_DLL变量填写的是你编译出的DLL的正确路径即可。
更简便的C++函数导出方案
如果你不想编写大量原生Python/C API绑定代码,有两种轻量方案可选,性能损耗几乎可以忽略,完全满足实时性要求:
方案1:使用pybind11绑定(推荐)
pybind11是header-only的C++/Python绑定库,不需要额外依赖,只需要少量代码即可完成C++函数、类的Python导出,无需处理GIL、引用计数等底层细节,导出的模块可以直接在Python中import,不需要通过ctypes加载。
示例代码如下:
#include <pybind11/pybind11.h> #include <pybind11/stl.h> namespace py = pybind11; // 你的原有C++函数,不需要做任何修改 void your_original_high_perf_func() { // 原有实时逻辑 } PYBIND11_MODULE(perf_utils, m) { m.doc() = "高性能C++工具模块"; // 导出原有函数即可 m.def("your_original_high_perf_func", &your_original_high_perf_func, "高性能处理函数"); // 要返回Python列表也可以直接用py::list,不需要手动处理引用计数 m.def("test3", []() { return py::list(10); }); }
编译后得到的.pyd文件直接放到Python脚本目录,执行import perf_utils即可调用导出的函数。
方案2:导出纯C接口,Python侧做类型转换
如果你不想在C代码中引入任何Python相关的依赖,可以直接导出纯C类型的接口,返回数组、结构体、基础数值类型,Python侧用ctypes接收后再转换为Python对象。这种方案完全不需要处理Python API相关的逻辑,出错概率更低。
比如要返回长度为10的double数组,C侧代码:
#ifdef _MSC_VER #define DLL_EXPORT __declspec( dllexport ) #else #define DLL_EXPORT #endif #ifdef __cplusplus extern "C"{ #endif // 导出函数返回数组指针,同时输出数组长度 DLL_EXPORT double* test3(int* out_len) { *out_len = 10; double* arr = (double*)malloc(*out_len * sizeof(double)); // 给arr赋值的逻辑 for(int i=0; i<*out_len; i++) arr[i] = i; return arr; } // 配套导出内存释放函数,避免内存泄漏 DLL_EXPORT void free_mem(void* ptr) { free(ptr); } #ifdef __cplusplus } #endif
Python侧接收代码:
import ctypes if __name__ == "__main__": lib = ctypes.cdll.LoadLibrary(LIB_DLL) lib.test3.argtypes = [ctypes.POINTER(ctypes.c_int)] lib.test3.restype = ctypes.POINTER(ctypes.c_double) lib.free_mem.argtypes = [ctypes.c_void_p] out_len = ctypes.c_int() arr_ptr = lib.test3(ctypes.byref(out_len)) # 转换为Python列表 res = [arr_ptr[i] for i in range(out_len.value)] # 释放C++侧申请的内存 lib.free_mem(arr_ptr) print(res)
注意事项
- 用ctypes返回
py_object类型时,Python会自动接管返回的PyObject的引用计数,不要在C++侧手动调用Py_DECREF,否则会出现重复释放崩溃。 - 所有涉及跨语言调用的场景,都要保证编译出的DLL和运行环境的架构、依赖版本完全一致,避免出现奇奇怪怪的加载错误。
内容的提问来源于stack exchange,提问作者hwgy
相关产品推荐
相关产品推荐

