如何正确地将长寿命缓冲区从C#发送到C++
How to correctly send long-live buffer from C# to 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[]是否可行)?
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();
}
}
}
相关文章:
- C++字符*缓冲区的大小
- 为什么msgrcv()将垃圾字符馈送到缓冲区
- 使用动态分配的数组会导致代码分析发出虚假的C6386缓冲区溢出警告
- ostream过载时的缓冲区冲洗
- C++中的高效循环缓冲区,它将被传递给C样式数组函数参数
- Xaudio2在更改缓冲区或循环时弹出声音
- 为什么我在leetcode上收到AddressSanitizer:地址0x602000000058上的堆缓冲区溢出错误
- 如何将图像传输到c++(dll)中的缓冲区,然后在c#的缓冲区中读/写
- 如何在cpp.中使用协议缓冲区存储大缓冲区/数组(char/int)
- 多线程双缓冲区
- Android P-9.0.0_r53 Logcat主缓冲区超出定义大小
- 套接字读取后,我在缓冲区中看到意外输入
- std::带有自定义缓冲区的 iostream 不允许我写入
- 从返回的顶点缓冲区查询顶点结构
- Vulkan 中的动态顶点缓冲区格式设置
- OpenGL 16 位模板缓冲区?
- 在 leetcode 上提交解决方案时出现堆栈缓冲区溢出错误
- 在 openGL 中多次绑定缓冲区
- struct.error:解压缩 C++ 结构时,解包需要 288 字节的缓冲区
- 为什么缓冲区的内容在读取空 rdbuf 时被删除?