使用Python ctypes调用带py_object参数的C++函数触发访问冲突
Python ctypes调用C++ DLL时PyArg_ParseTuple触发访问冲突的问题分析与解决
问题场景
在Python 3.9 + MSVC 2022环境下,通过ctypes调用C++ DLL中接收py_object参数的My_Function3函数时触发访问冲突。该函数尝试用PyArg_ParseTuple从传入的PyObject提取字符串,但执行到该步骤时报错。此前传递c_char_p给对应参数的函数并返回PyObject*的操作可正常运行,且已确认地址传递正确。后续排查发现问题出在异常处理与参数类型匹配环节,调整字符串复制逻辑后以扩展形式正常运行。
问题根源
- PyArg_ParseTuple使用场景错误:该函数是为Python扩展模块设计的,要求传入的参数是参数元组(比如Python侧调用
func(a,b)时,C侧收到的是包含a和b的元组)。但当前Python代码直接传递单个字符串对象,C侧将其当作元组解析,必然导致内存访问错误。 - 未初始化异常对象:全局变量
MyError未初始化,调用PyErr_SetString时会访问空指针,加剧错误。 - 未初始化局部数组:局部数组
s未初始化就调用strcat_s,会导致未定义行为。
修复方案
方案1:直接处理单个字符串对象(推荐)
跳过PyArg_ParseTuple,直接将传入的PyObject(Python字符串)转换为C字符串:
修复后的C++代码
#include "pch.h" #define PY_SSIZE_T_CLEAN #include <Python.h> extern "C" { __declspec(dllexport) PyObject* __stdcall My_Function3(PyObject *str_obj) { // 检查传入对象是否为字符串 if (!PyUnicode_Check(str_obj)) { PyErr_SetString(PyExc_TypeError, "Expected a string"); return NULL; } // 将Python字符串转换为UTF-8编码的C字符串 const char* str = PyUnicode_AsUTF8(str_obj); if (!str) { return NULL; // 转换失败时已自动设置异常 } // 检查字符串长度 size_t str_len = strlen(str); if (str_len >= 100) { PyErr_SetString(PyExc_ValueError, "Error: string too long"); return NULL; } // 初始化局部数组并复制字符串 char s[100] = {0}; strcpy_s(s, 100, str); return Py_BuildValue("s", s); } }
Python代码(无需修改)
import ctypes import sys import os y = __file__ str_path = os.path.dirname(os.path.realpath("..\\..\\string\\x64\\Debug\\string.dll")) handle = ctypes.CDLL(str_path + "/string.dll") handle.My_Function3.argtypes = [ctypes.py_object] handle.My_Function3.restype = ctypes.py_object z = handle.My_Function3(y) print(z)
方案2:适配PyArg_ParseTuple的元组参数
如果坚持使用PyArg_ParseTuple,Python侧需要传递参数元组:
修改后的Python代码
import ctypes import sys import os y = __file__ str_path = os.path.dirname(os.path.realpath("..\\..\\string\\x64\\Debug\\string.dll")) handle = ctypes.CDLL(str_path + "/string.dll") handle.My_Function3.argtypes = [ctypes.py_object] handle.My_Function3.restype = ctypes.py_object # 传递参数元组而非单个字符串 z = handle.My_Function3((y,)) print(z)
修复后的C++代码(初始化异常对象)
#include "pch.h" #define PY_SSIZE_T_CLEAN #include <Python.h> static PyObject *MyError; // DLL初始化时注册自定义异常类型 BOOL APIENTRY DllMain(HMODULE hModule, DWORD ul_reason_for_call, LPVOID lpReserved) { switch (ul_reason_for_call) { case DLL_PROCESS_ATTACH: MyError = PyErr_NewException("mydll.MyError", NULL, NULL); break; case DLL_THREAD_ATTACH: case DLL_THREAD_DETACH: case DLL_PROCESS_DETACH: break; } return TRUE; } extern "C" { __declspec(dllexport) PyObject* __stdcall My_Function3(PyObject *args) { const char* str; char s[100] = {0}; // 初始化局部数组 // 解析元组中的单个字符串参数 if (!PyArg_ParseTuple(args, "s", &str)) return NULL; if (strlen(str) >= 100) { PyErr_SetString(MyError, "Error: string too long"); return NULL; } strcpy_s(s, 100, str); return Py_BuildValue("s", s); } }
关键注意点
- 区分Python扩展模块和ctypes调用的差异:扩展模块的函数参数是元组,而ctypes直接传递单个对象时,C侧收到的是该对象本身,不是元组。
- 始终初始化全局异常对象,或直接使用Python标准异常类型(如
PyExc_ValueError、PyExc_TypeError)。 - 局部数组必须初始化后再使用,避免未定义行为。
内容的提问来源于stack exchange,提问作者user23317703
相关产品推荐
相关产品推荐

