使用Interop将C++的std::map导出到C#时遇访问冲突问题求助
跨C++/C#调用时std::map赋值引发访问违规的问题解决
问题重现
C++导出函数代码
extern "C" __declspec(dllexport) bool CallCustomObjectGetPropertyValue(AcDbObject* obj, long objId, std::map<std::string, std::vector<std::string>>& mapToFill) { CustomObject* custom = dynamic_cast<CustomObject*>(obj); if (!custom) { acutPrintf(_T("\nERROR: custom object is nullptr")); return false; } std::map<std::string, std::vector<std::string>> EmptyMapToFill = {}; mapToFill = EmptyMapToFill; }
C#调用代码
[DllImport("SheetMetalSample.brx")] private static extern void CallCustomObjectGetPropertyValue(IntPtr obj, long objId, out IntPtr Int32mapPtr);
执行时触发错误:read access violation. _Head was 0x400,但赋值char、string类型时无异常。
问题根源
核心矛盾是C++标准库容器(如std::map)无法直接与C#的IntPtr进行跨语言交互:
- std::map是C++专属的复杂容器,内部包含指针、节点结构等依赖编译器实现的内存布局,没有统一的C兼容标准。
- C#端传递的
out IntPtr只是一个原始内存地址,并非一个已正确构造的std::map对象,C++函数接收的引用绑定到了无效内存区域,赋值操作直接触发内存越界。 - char/string看似正常是巧合:char是基础类型,string如果是简单传递指针可能暂时未触发崩溃,但本质上同样存在内存管理和布局不兼容的风险。
解决方案
跨语言DLL调用必须使用C兼容的类型和内存传递方式,以下是两种可行方案:
方案一:序列化数据为字节数组(推荐)
将C++的map数据序列化为通用格式(如JSON),传递内存块指针给C#,C#解析后再释放内存。
C++端代码
#include <nlohmann/json.hpp> // 可替换为其他JSON库或自定义二进制序列化 using json = nlohmann::json; extern "C" __declspec(dllexport) bool CallCustomObjectGetPropertyValue(AcDbObject* obj, long objId, char** outData, size_t* outSize) { CustomObject* custom = dynamic_cast<CustomObject*>(obj); if (!custom) { acutPrintf(_T("\nERROR: custom object is nullptr")); return false; } // 填充数据到json对象(示例) json j; // j["property1"] = custom->getProperty1(); // j["property2"] = {"val1", "val2"}; std::string jsonStr = j.dump(); *outSize = jsonStr.size(); *outData = new char[*outSize + 1]; strcpy_s(*outData, *outSize + 1, jsonStr.c_str()); return true; } // 必须导出内存释放函数,避免泄漏 extern "C" __declspec(dllexport) void FreeSerializedData(char* data) { delete[] data; }
C#端代码
using System.Text.Json; using System.Runtime.InteropServices; [DllImport("SheetMetalSample.brx", CallingConvention = CallingConvention.Cdecl)] private static extern bool CallCustomObjectGetPropertyValue(IntPtr obj, long objId, out IntPtr outData, out UInt32 outSize); [DllImport("SheetMetalSample.brx", CallingConvention = CallingConvention.Cdecl)] private static extern void FreeSerializedData(IntPtr data); public static Dictionary<string, List<string>> GetCustomProperties(IntPtr obj, long objId) { if (!CallCustomObjectGetPropertyValue(obj, objId, out IntPtr dataPtr, out uint size)) { return null; } string jsonStr = Marshal.PtrToStringAnsi(dataPtr, (int)size); FreeSerializedData(dataPtr); // 必须调用C++端的释放函数 return JsonSerializer.Deserialize<Dictionary<string, List<string>>>(jsonStr); }
方案二:使用C风格结构体数组传递
定义C兼容的结构体,将map的键值对转换为数组传递。
C++端代码
// C兼容的结构体定义 typedef struct { const char* key; const char** values; int valueCount; } PropertyEntry; extern "C" __declspec(dllexport) bool CallCustomObjectGetPropertyValue(AcDbObject* obj, long objId, PropertyEntry** outEntries, int* outEntryCount) { CustomObject* custom = dynamic_cast<CustomObject*>(obj); if (!custom) { acutPrintf(_T("\nERROR: custom object is nullptr")); return false; } std::map<std::string, std::vector<std::string>> dataMap; // 填充dataMap(示例) // dataMap["example"] = {"val1", "val2"}; *outEntryCount = dataMap.size(); *outEntries = new PropertyEntry[*outEntryCount]; int idx = 0; for (auto& pair : dataMap) { (*outEntries)[idx].key = _strdup(pair.first.c_str()); int valCount = pair.second.size(); (*outEntries)[idx].valueCount = valCount; (*outEntries)[idx].values = new const char*[valCount]; for (int i = 0; i < valCount; i++) { (*outEntries)[idx].values[i] = _strdup(pair.second[i].c_str()); } idx++; } return true; } // 释放结构体数组的内存 extern "C" __declspec(dllexport) void FreePropertyEntries(PropertyEntry* entries, int entryCount) { for (int i = 0; i < entryCount; i++) { free((void*)entries[i].key); for (int j = 0; j < entries[i].valueCount; j++) { free((void*)entries[i].values[j]); } delete[] entries[i].values; } delete[] entries; }
C#端代码
using System.Runtime.InteropServices; [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)] private struct PropertyEntry { public IntPtr key; public IntPtr values; // 指向string数组的指针 public int valueCount; } [DllImport("SheetMetalSample.brx", CallingConvention = CallingConvention.Cdecl)] private static extern bool CallCustomObjectGetPropertyValue(IntPtr obj, long objId, out IntPtr outEntries, out int outEntryCount); [DllImport("SheetMetalSample.brx", CallingConvention = CallingConvention.Cdecl)] private static extern void FreePropertyEntries(IntPtr entries, int entryCount); public static Dictionary<string, List<string>> GetCustomProperties(IntPtr obj, long objId) { if (!CallCustomObjectGetPropertyValue(obj, objId, out IntPtr entriesPtr, out int entryCount)) { return null; } var result = new Dictionary<string, List<string>>(); int entrySize = Marshal.SizeOf<PropertyEntry>(); for (int i = 0; i < entryCount; i++) { IntPtr entryPtr = IntPtr.Add(entriesPtr, i * entrySize); PropertyEntry entry = Marshal.PtrToStructure<PropertyEntry>(entryPtr); string key = Marshal.PtrToStringAnsi(entry.key); var values = new List<string>(); for (int j = 0; j < entry.valueCount; j++) { IntPtr valuePtr = Marshal.ReadIntPtr(entry.values, j * IntPtr.Size); values.Add(Marshal.PtrToStringAnsi(valuePtr)); } result[key] = values; } FreePropertyEntries(entriesPtr, entryCount); // 释放内存 return result; }
关键注意事项
- 严格使用C兼容类型:跨语言调用禁止传递C++类、容器的引用或指针,不同编译器的内存布局和名称修饰规则不一致会导致崩溃。
- 统一内存管理:C端分配的内存必须通过C导出的函数释放,不能在C#端直接释放,避免内存泄漏或堆损坏。
- 指定调用约定:C++默认使用
cdecl调用约定,C#的DllImport必须显式指定,否则会导致栈溢出或参数解析错误。
内容的提问来源于stack exchange,提问作者Guinea Pig Taco
相关产品推荐
相关产品推荐

