如何正确地将C#中的长活缓冲区发送到C ++

时间:2015-11-03 19:10:13

标签: c# c++

任务 - 提供用于从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 [])?

2 个答案:

答案 0 :(得分:1)

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 \0 
 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.

答案 1 :(得分:0)

从[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 \0 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();
        }
    }
}