任务 - 提供用于从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 [])?
答案 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();
}
}
}