C#通过P/Invoke调用C++ DLL时如何正确接收回调的字符串参数
问题原因
std::string是C++标准库定义的非原生互操作类型,内存布局随编译器版本、编译配置、运行库选项变化,不属于P/Invoke可直接自动封送的类型,直接映射为C#string会读取到错误的内存偏移,直接导致乱码。- 委托和P/Invoke导入定义未显式指定调用约定、字符集与封送规则,默认封送逻辑会按错误的编码解析内存。
- 若委托仅声明为局部变量,会被GC意外回收,后续C++侧触发回调时会出现内存访问错误,当前乱码问题暂未触发该隐患。
解决方案
方案1:修改C++库侧接口(生产环境推荐,稳定性最高)
跨DLL互操作的通用规则是不要跨边界传递C++标准库类型,统一用C风格原生类型做参数。
- 修改C++侧的回调签名,将
std::string参数替换为C风格字符串指针:// 原错误签名 // typedef void (*callback_func)(std::string log, int time); // 修改后签名,默认按UTF8编码传参 typedef void (*callback_func)(const char* log, int time); // 调用回调的位置,把std::string转成const char*传入即可 // 原调用逻辑:callback(your_log_str, time); // 修改为: callback(your_log_str.c_str(), time); - 修正C#侧的委托与P/Invoke定义,显式指定封送规则,同时持有委托引用避免被GC回收:
// 显式指定调用约定与字符串封送规则 [UnmanagedFunctionPointer(CallingConvention.Cdecl, CharSet = CharSet.Ansi)] public delegate void log_delegate( [MarshalAs(UnmanagedType.LPUTF8Str)] string log, int time ); static void logFunc(string log, int time) { Console.WriteLine($"{log} {time}"); } // 把委托存为类级别的静态/实例成员,不要只用局部变量,防止被GC回收 private static log_delegate _callbackKeepAlive; // 对应Foo_Create的P/Invoke导入定义 [DllImport("你的C++库文件名.dll", CallingConvention = CallingConvention.Cdecl)] public static extern IntPtr Foo_Create(int param1, int param2, log_delegate callback); // 调用示例 static void Main() { _callbackKeepAlive = logFunc; IntPtr foo = Foo_Create(6, 6, _callbackKeepAlive); // 后续业务逻辑 }如果你的C库编译时指定了
__stdcall调用约定,把上述代码里所有的CallingConvention.Cdecl替换为CallingConvention.StdCall即可;如果C侧字符串是GBK编码,把[MarshalAs(UnmanagedType.LPUTF8Str)]替换为[MarshalAs(UnmanagedType.LPStr)]。
方案2:无C++库源码时的临时应急方案
如果你无法修改C++库代码,只能手动解析std::string的内存结构,该方案兼容性极差,仅适合临时测试使用:
- 先确认C++库的编译环境:编译器版本、目标平台(x86/x64)、编译配置,不同环境下
std::string的内存布局完全不同。以最常见的VS2015及以上版本MSVC编译的x64 Release库为例,std::string起始偏移0位置在长字符串场景下存的是char*类型的字符串指针。 - 修改C#侧委托,将第一个参数改为
IntPtr手动读取内存:
该方案没有通用适配性,只要C++库的编译环境变化就会出现乱码甚至程序崩溃,不要在生产环境使用。[UnmanagedFunctionPointer(CallingConvention.Cdecl)] public delegate void log_delegate(IntPtr stdStringPtr, int time); static void logFunc(IntPtr stdStringPtr, int time) { // 读取std::string内部的字符串指针 IntPtr rawStrPtr = Marshal.ReadIntPtr(stdStringPtr); // 按对应编码转成C#字符串,UTF8用PtrToStringUTF8,GBK用PtrToStringAnsi string log = Marshal.PtrToStringUTF8(rawStrPtr); Console.WriteLine($"{log} {time}"); }
内容的提问来源于stack exchange,提问作者user19435638
相关产品推荐
相关产品推荐

