You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

C#通过P/Invoke调用C++ DLL时如何正确接收回调的字符串参数

问题原因
  • std::string是C++标准库定义的非原生互操作类型,内存布局随编译器版本、编译配置、运行库选项变化,不属于P/Invoke可直接自动封送的类型,直接映射为C# string会读取到错误的内存偏移,直接导致乱码。
  • 委托和P/Invoke导入定义未显式指定调用约定、字符集与封送规则,默认封送逻辑会按错误的编码解析内存。
  • 若委托仅声明为局部变量,会被GC意外回收,后续C++侧触发回调时会出现内存访问错误,当前乱码问题暂未触发该隐患。
解决方案

方案1:修改C++库侧接口(生产环境推荐,稳定性最高)

跨DLL互操作的通用规则是不要跨边界传递C++标准库类型,统一用C风格原生类型做参数。

  1. 修改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);
    
  2. 修正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的内存结构,该方案兼容性极差,仅适合临时测试使用:

  1. 先确认C++库的编译环境:编译器版本、目标平台(x86/x64)、编译配置,不同环境下std::string的内存布局完全不同。以最常见的VS2015及以上版本MSVC编译的x64 Release库为例,std::string起始偏移0位置在长字符串场景下存的是char*类型的字符串指针。
  2. 修改C#侧委托,将第一个参数改为IntPtr手动读取内存:
    [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}");
    }
    
    该方案没有通用适配性,只要C++库的编译环境变化就会出现乱码甚至程序崩溃,不要在生产环境使用。

内容的提问来源于stack exchange,提问作者user19435638

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.08.28 14:06:29