C#调用C++ DLL触发System.AccessViolationException的问题求助
我编写了一段C++代码,想要打包为DLL并在C# Windows Forms应用中调用。目前已生成DLL,但调用时触发System.AccessViolationException(最初连DLL都加载失败,解决加载问题后出现了这个访问冲突错误)。
团队需要把这段C代码集成到Windows Forms应用里,我正好借此学习C DLL项目以及和C#/Windows Forms的集成。参照YouTube教程写了如下DLL代码:
extern "C" _declspec(dllexport) Communication* CreateCommunicationObject(const char* ip, unsigned int p_out, unsigned int p_in) { return &Communication::Communication(ip, p_out, p_in); } extern "C" _declspec(dllexport) st_rtioout getReceivedData(Communication *comm) { return comm->getData(); } extern "C" _declspec(dllexport) void sendNewData(Communication *comm, st_rtioin *data) { return comm->sendData(*data); } extern "C" _declspec(dllexport) char* getReceptionStatus(Communication *comm) { return comm->getReceptionStatus(); } extern "C" _declspec(dllexport) char* getSendingStatus(Communication *comm) { return comm->getSendingStatus(); }
生成DLL后,尝试在C#窗体应用中添加引用时,收到错误:无法添加对project2.dll的引用,请确保文件可访问且是有效的程序集或COM组件。查了Stack Overflow的方案后,写了如下C#代码:
[DllImport("Project2.dll", EntryPoint = "CreateCommunicationObject", CallingConvention = CallingConvention.StdCall)] static extern IntPtr CreateCommunicationObject(string ip, int port_out, int port_in); [DllImport("Project2.dll", EntryPoint = "getReceivedData", CallingConvention = CallingConvention.StdCall)] static extern st_rtioout getReceivedData(IntPtr ptr); [DllImport("Project2.dll", EntryPoint = "sendNewData", CallingConvention = CallingConvention.StdCall)] static extern void sendNewData(IntPtr ptr, st_rtioin sendingdata); [DllImport("Project2.dll", EntryPoint = "getReceptionStatus", CallingConvention = CallingConvention.StdCall)] static extern string getReceptionStatus(IntPtr ptr); [DllImport("Project2.dll", EntryPoint = "getSendingStatus", CallingConvention = CallingConvention.StdCall)] static extern string getSendingStatus(IntPtr ptr);
但调用CreateCommunicationObject时触发了System.AccessViolationException(提示尝试读取或写入受保护的内存,通常表示其他内存已损坏)。我用的是VS2013,请问问题出在哪?该怎么修正?
编辑:
参考评论和建议修改了代码,但还是出现相同错误。
新的C++代码:
extern "C" _declspec(dllexport) void Initialize(std::string ip, unsigned int p_out, unsigned int p_in) { const char* ipaddress = ip.c_str(); Communication comms = Communication(ipaddress, p_out, p_in); Comms = &comms; } extern "C" _declspec(dllexport) st_rtioout getReceivedData() { return Comms->getData(); } extern "C" _declspec(dllexport) void sendNewData(st_rtioin data) { Comms->sendData(data); } extern "C" _declspec(dllexport) std::string getReceptionStatus() { std::string str = Comms->getReceptionStatus(); return str; } extern "C" _declspec(dllexport) std::string getSendingStatus() { std::string str = Comms->getSendingStatus(); return str; }
新的C#代码:
[DllImport("Project2.dll", EntryPoint = "Initialize", CallingConvention = CallingConvention.Cdecl)] static extern void Initialize(string ip, int port_out, int port_in); [DllImport("Project2.dll", EntryPoint = "getReceivedData", CallingConvention = CallingConvention.Cdecl)] static extern st_rtioout getReceivedData(); [DllImport("Project2.dll", EntryPoint = "sendNewData", CallingConvention = CallingConvention.Cdecl)] static extern void sendNewData(st_rtioin sendingdata); [DllImport("Project2.dll", EntryPoint = "getReceptionStatus", CallingConvention = CallingConvention.Cdecl)] static extern string getReceptionStatus(); [DllImport("Project2.dll", EntryPoint = "getSendingStatus", CallingConvention = CallingConvention.Cdecl)] static extern string getSendingStatus();
问题根源与修复方案
1. 初始代码的核心问题
(1)构造函数调用错误
最初的CreateCommunicationObject里,&Communication::Communication(ip, p_out, p_in)是错误写法:直接调用构造函数会创建临时对象,函数返回后临时对象立即销毁,返回的指针指向已释放的内存,后续调用必然触发访问冲突。
(2)调用约定不匹配
C++默认调用约定是cdecl,但最初的C#代码用了StdCall,调用约定不匹配会破坏栈结构,引发内存错误。
(3)字符串类型跨语言不兼容
直接返回char*或std::string到C#存在风险:std::string是C++非托管类型,C#无法直接解析;char*若指向DLL内部栈内存,会随函数返回被释放,导致野指针。
2. 修改后代码的新问题
新写的Initialize函数中,Communication comms = Communication(...)创建的是栈上局部对象,函数执行完毕后对象被销毁,全局指针Comms指向已释放的内存,后续调用getReceivedData等函数时访问无效指针,依然触发访问冲突。
正确的修复步骤
步骤1:正确创建和销毁Communication对象
必须在堆上分配Communication对象,避免局部对象销毁导致的野指针:
// 全局指针保存对象实例(多线程场景需加锁保证线程安全) Communication* Comms = nullptr; extern "C" _declspec(dllexport) bool Initialize(const char* ip, unsigned int p_out, unsigned int p_in) { // 先释放已有实例,避免内存泄漏 if (Comms != nullptr) { delete Comms; Comms = nullptr; } try { Comms = new Communication(ip, p_out, p_in); return true; } catch (...) { return false; } } // 添加销毁函数,避免内存泄漏 extern "C" _declspec(dllexport) void DestroyCommunication() { if (Comms != nullptr) { delete Comms; Comms = nullptr; } }
步骤2:统一调用约定
C++端保持默认cdecl,C#端DllImport也指定CallingConvention = CallingConvention.Cdecl,避免栈破坏。
步骤3:字符串类型的安全传递
不能直接返回std::string或栈上char*,推荐两种安全方式:
- 方式一:由C#分配缓冲区,DLL将字符串复制到缓冲区
extern "C" _declspec(dllexport) void getReceptionStatus(char* buffer, int bufferSize) { if (Comms == nullptr || buffer == nullptr || bufferSize <= 0) return; std::string status = Comms->getReceptionStatus(); // 复制字符串到缓冲区,确保不越界 strncpy_s(buffer, bufferSize, status.c_str(), _TRUNCATE); }
C#端调用:
[DllImport("Project2.dll", CallingConvention = CallingConvention.Cdecl)] static extern void getReceptionStatus(StringBuffer buffer, int bufferSize); // 使用示例 var buffer = new StringBuffer(256); getReceptionStatus(buffer, buffer.Capacity); string status = buffer.ToString();
- 方式二:返回由
CoTaskMemAlloc分配的字符串,C#会自动释放内存
#include <comutil.h> #pragma comment(lib, "comsuppw.lib") extern "C" _declspec(dllexport) BSTR getReceptionStatus() { if (Comms == nullptr) return nullptr; std::string status = Comms->getReceptionStatus(); // 转换为BSTR,由C#自动释放 return _com_util::ConvertStringToBSTR(status.c_str()); }
C#端调用:
[DllImport("Project2.dll", CallingConvention = CallingConvention.Cdecl)] [return: MarshalAs(UnmanagedType.BStr)] static extern string getReceptionStatus();
步骤4:结构体的内存布局匹配
确保C#端的st_rtioout和st_rtioin结构体与C++端内存布局完全一致:
- C++端结构体指定对齐方式,比如
#pragma pack(push, 1) - C#端结构体添加
[StructLayout(LayoutKind.Sequential, Pack = 1)](根据C++对齐方式调整Pack值)
示例:
C++端:
#pragma pack(push, 1) struct st_rtioout { int field1; float field2; // 其他字段 }; #pragma pack(pop)
C#端:
[StructLayout(LayoutKind.Sequential, Pack = 1)] struct st_rtioout { public int field1; public float field2; // 对应C++的其他字段 };
步骤5:DLL部署注意事项
- 确保C#项目的平台目标(x86/x64)与C++ DLL的编译平台一致,避免位数不匹配
- 将DLL复制到C#项目的输出目录(如
bin/Debug),或者在DllImport中指定完整路径
内容的提问来源于stack exchange,提问作者Flight Dynamic Model

