如何正确地将长寿命缓冲区从C#发送到C++

How to correctly send long-live buffer from C# to C++

本文关键字:C++ 缓冲区 正确地      更新时间:2023-10-16

任务-提供缓冲区,用于从C++DLL注销并从C#使用

我做了什么(这是错误的)C++伪代码:

wchar_t* _out;
int _outsize;
extern "C" __declspec(dllexport) void setOut(wchar_t* out, int outsize){
  _out = out;
  _outsize = outsize;
}
void logOut(wstring& message){
    const wchar_t* m = message.c_str();
    wcscat_s(_out,_outsize,m);
}
extern "C" __declspec(dllexport) void worker() {
    logOut(L"start");
    doWork();
    logOut(L"finish");
}

我在C#中做了什么(通常用于字符串外)

[DllImport(LibraryName, CallingConvention = CallingConvention.StdCall, CharSet = CharSet.Unicode)]
extern public static void setOut([MarshalAs(UnmanagedType.LPWStr)]StringBuilder buf, int size);
[DllImport(LibraryName, CallingConvention = CallingConvention.StdCall)]
extern public static void worker();
public void Run(){
    const int bufsize = 1024;
    var buf = new StringBuilder(bufsize);
    setOut(buf, bufsize);
    doWork();
    Assert.True(buf.ToString().Contains("start"));
}

它失败了——有两种变体——有时进程中止,有时缓冲区为空。

在调试过程中,我发现这是我的一个大错误——setOut中的wchar_t*参数只有在第一次调用setOut时才是有效的指针(所以如果我在其中写一些东西——我会在调用者中捕获数据)。但在此之后,在第二次调用发生之前(worker())指针变为无效。

因此,我可以看到StringBuilder被整理为单个PInvoke循环的临时指针。

那么,它是以某种方式提供长寿命CLI<->的方法吗C++字符串缓冲区?(我知道我可以使用文件、映射文件或tcp套接字,但这不是很好,问题是使用StringBuilder、char[]或byte[]是否可行)?

Marshaling分配一个单独的临时缓冲区(这就是out所指向的),然后在函数出口上将其内容复制到StringBuilder中。

由于这种行为,out指针仅可用于通过将字符串复制到该指针中(例如通过从相同或嵌套函数中调用wcscat_s(out, outsize,m))来传递回数据;

要修复代码,请考虑更改worker()签名,将out参数直接传递给该函数:

 void worker(wchar_t* out, size_t outsize) 
 {
     setOut(out, outsize);
     ....
     setOut(NULL, 0);
}

另一个[不那么优雅]的选项是在代码中显式封送:

  [DllImport(LibraryName, CallingConvention = CallingConvention.StdCall)]
  extern public static void setOut(IntPtr buf, int size);

 char[] buffer = new char[bufferSize];
 // Specify size in bytes leaving room for terminal  
 IntPtr outPtr = Marshal.AllocHGlobal((bufferSize + 1) * sizeof(char) );
 try 
 {
    setOut(outPtr, bufferSize);
    worker();
    setOut(IntPtr.Zero, 0);
    Marshal.Copy(outPtr, buffer, 0, bufferSize); 
 }
 finally
 {
    Marshal.FreeHGlobal(outPtr);
 }
 // use buffer.

在[alexm]给出明智的答案后,我编写了特殊的OutStringBuffer类来实现长寿命缓冲区。在这里,我将其作为片段发布,同时它运行良好且安全:

/// <summary>
/// Intermediate buffer to be used as out string parameter in interop scenario
/// </summary>
/// <remarks>
/// It's not writeable or initializable with string at caller site to exactly match
/// task of retrieve string data from called site.
/// Can work both with ANSI and Unicode (16/32), so with char* and wchar_t* c++ buffers
/// Can be used as Long-Live buffer 
/// </remarks>
/// <example>
/// /// we not require to use StringBuilder and set CharSet explicitly
/// [DllImport(LibraryName, CallingConvention = CallingConvention.StdCall)]
/// public static extern void setBuffer(IntPtr sb, int capacity);
/// [DllImport(LibraryName, CallingConvention = CallingConvention.StdCall)]
/// public static extern void doSomethingThatWritesToBuffer();
/// using (var buffer = new OutStringBuffer(100)){
///     setBuffer(buffer.GetIntPtr(), buffer.GetCapacity());
///     doSomethingThatWritesToBuffer();
///     Console.WriteLine(buffer.ToString());
/// }
/// </example>
public class OutStringBuffer : IDisposable { // it must be disposal while it allocates unmanaged memory
    public const int ANSI_SIZE = 1;
    public const int UTF16_SIZE = 2;
    public const int UTF32_SIZE = 4;
    // be default most implementation of c++ uses UTF-16 for wchar_t and .net is Unicode-16 based system by default
    // if you use C++ implementation with 4-byte wide char u can define WCHAR_UTF32 globally
#if WCHAR_UTF32
    public const int DEFAULT_WCHARSIZE = UTF32_SIZE;
#else
    public const int DEFAULT_WCHARSIZE = UTF16_SIZE;
#endif
    /// <summary>
    /// Initialize buffer of given size (in chars) with given char_size
    /// </summary>
    /// <param name="size">Size of buffer in chars (5 for "Hello")</param>
    /// <param name="charsize">Size of used char (Unicode in platform C++ by default) 1(ANSI) 2(UTF-16) or 4(UTF-32)</param>
    public OutStringBuffer(int size, int charsize = DEFAULT_WCHARSIZE)
    {
        if (size <= 0)
        {
            throw new ArgumentException(INVALID_SIZE_MESSAGE, nameof(size));
        }
        if (!(charsize == ANSI_SIZE || charsize == UTF16_SIZE || charsize == UTF32_SIZE))
        {
            throw new ArgumentException(INVALID_CHARSIZE_MESSAGE, nameof(charsize));
        }
        _size = size;
        _charsize = charsize;
    }

    private readonly int _size = 0;
    private IntPtr _ptr = IntPtr.Zero;
    private bool _disposed = false;
    private readonly int _charsize;
    /// <summary>
     /// Retruns given max size in chars
     /// </summary>
     /// <returns>Max size of out string</returns>
    public int GetCapacity() {
        return _size;
    }
    private const string INVALID_SIZE_MESSAGE = "Size must be greater than zero";
    private const string INVALID_CHARSIZE_MESSAGE = "Char size must be 1 for ANSI or 2 for UTF-16 (MC++ wchar_t) or 4 for UTF-32";

    /// <summary>
    /// Initializes global IntPtr for string buffer, that can be used in Interop
    /// </summary>
    /// <returns>Pointer to char* or wchar_t* with given size</returns>
    /// <remarks>It's lazy - if not called - OutStringBuffer will not allocate memory</remarks>
    public IntPtr GetIntPtr() {
        if (_disposed) {
            throw new ObjectDisposedException("this buffer was disposed");
        }
        if (_ptr == IntPtr.Zero) {
            _ptr = Marshal.AllocHGlobal((_size + 1) * _charsize); // correct size for wchar_t* (null-terminated)
            // we require to initialize first char with  while if we not - it can cause garbage at start of string
            if (_charsize == ANSI_SIZE) {
                Marshal.WriteByte(_ptr,0);
            }else if (_charsize == UTF16_SIZE) {
                Marshal.WriteInt16(_ptr, 0);
            }
            else {
                Marshal.WriteInt32(_ptr, 0);
            }
        }
        return _ptr;
    }
    /// <summary>
    /// Read current string value from unmanaged buffer
    /// </summary>
    /// <returns>current string data from buffer</returns>
    public override string ToString() {
        if (_ptr == IntPtr.Zero) {
            return string.Empty;
        }
        if (_charsize == ANSI_SIZE) {
            return Marshal.PtrToStringAnsi(_ptr) ?? string.Empty;
        }
        if (_charsize == UTF16_SIZE) {
            return Marshal.PtrToStringUni(_ptr) ?? string.Empty;
        }
        //don't found more efficient way to translate UTF-32 IntPtr to string 
        var b = new StringBuilder();
        for (var i = 0; i < _size; i++) {
            var _p = _ptr + i*UTF32_SIZE;
            var c = Marshal.ReadInt32(_p);
            if(c==0)break;
            b.Append(char.ConvertFromUtf32(c));
        }
        return b.ToString();
    }
    public void Dispose() {
        _disposed = true;
        if (_ptr != IntPtr.Zero) {
            Marshal.FreeHGlobal(_ptr);
        }
    }
    //while it can be used outside "using" block we force dispose at GC too
    ~OutStringBuffer() {
        if (!_disposed) {
            Dispose();
        }
    }
}