如何从PInvoke本机回调返回StringBuilder或其他字符串缓冲区
本文关键字:StringBuilder 其他 字符串 缓冲区 返回 回调 PInvoke 本机 | 更新日期: 2023-09-27 18:25:30
我想要一种干净的方法来增加StringBuilder()的大小,以满足本机代码填充的需要,下面的回调方法看起来很干净,但不知何故,我们得到了缓冲区的副本,而不是实际的缓冲区——我对解释和解决方案感兴趣(最好坚持回调类型的分配,因为如果它能工作的话,它会很好,很干净)。
using System;
using System.Runtime.InteropServices;
using System.Text;
namespace csharpapp
{
internal class Program
{
private static void Main(string[] args)
{
var buffer = new StringBuilder(12);
// straightforward, we can write to the buffer but unfortunately
// cannot adjust its size to whatever is required
Native.works(buffer, buffer.Capacity);
Console.WriteLine(buffer);
// try to allocate the size of the buffer in a callback - but now
// it seems only a copy of the buffer is passed to native code
Native.foo(size =>
{
buffer.Capacity = size;
buffer.Replace("works", "callback");
return buffer;
});
string s = buffer.ToString();
Console.WriteLine(s);
}
}
internal class Native
{
public delegate StringBuilder AllocateBufferDelegate(int bufsize);
[DllImport("w32.dll", CharSet = CharSet.Ansi)]
public static extern long foo(AllocateBufferDelegate callback);
[DllImport("w32.dll", CharSet = CharSet.Ansi)]
public static extern void works(StringBuilder buf, int bufsize);
}
}
本地标头
#ifdef W32_EXPORTS
#define W32_API __declspec(dllexport)
#else
#define W32_API __declspec(dllimport)
#endif
typedef char*(__stdcall *FnAllocStringBuilder)(int);
extern "C" W32_API long foo(FnAllocStringBuilder fpAllocate);
extern "C" W32_API void works(char *buf, int bufsize);
本机代码
#include "stdafx.h"
#include "w32.h"
#include <stdlib.h>
extern "C" W32_API long foo(FnAllocStringBuilder fpAllocate)
{
char *src = "foo X";
int len = strlen(src) + 1;
char *buf = fpAllocate(len);
return strcpy_s(buf,len,src);
}
extern "C" W32_API void works(char *buf, int bufsize)
{
strcpy_s(buf,bufsize,"works");
}
我有一个理论来解释为什么会发生这种情况。我怀疑StringBuilder
的编组包括制作数据的副本,将其传递给P/Invoke调用,然后将其复制回StringBuilder
。但我实际上无法证实这一点。
唯一的替代方案是首先对StringBuilder
进行展平(它在内部是char[]
的链表),并固定char[]
,即使这样,这也只能用于封送指向Unicode字符字符串的指针,而不能用于ANSI或COM字符串。
因此,当您传递一个StringBuilder
作为参数时,.NET有一个明显的位置可以将任何更改复制回来:在p/Invoke返回之后。
当您传递返回StringBuilder
的委托时,情况并非如此。在这种情况下,.NET需要创建一个包装器,将int => StringBuilder
函数转换为int => char*
函数。这个包装器将创建char*
缓冲区并填充它,但显然还不能将任何更改复制回来。在接受委托返回的函数之后,它也不能这样做:现在还为时过早!
事实上,根本没有明显的地方可以发生反向复制。
因此,我的猜测是:当编组返回StringBuilder
的委托时,.NET只能执行单向转换,因此您所做的任何更改都不会反映在StringBuilder
中。这比完全无法召集这样的代表要好一点。
至于解决方案:我建议首先询问本机代码缓冲区需要有多大,然后在第二次调用中传递适当大小的缓冲区。或者,如果您需要更好的性能,可以猜测一个足够大的缓冲区,但允许本机方法传达需要更多空间的信息。这样,大多数调用将只涉及一个P/Invoke转换。
这可以封装到一个更方便的函数中,您可以直接从托管世界调用该函数,而无需担心缓冲区。
小时>除了romkyns提供的输入之外,我还将分享我提出的最小更改解决方案。如果有人用这个,小心你的编码!
主要的修改是:
private static void Main(string[] args)
{
byte[] bytes = null;
var gcHandle = new GCHandle();
Native.foo(size =>
{
bytes = new byte[size];
gcHandle = GCHandle.Alloc(bytes,GCHandleType.Pinned);
return gcHandle.AddrOfPinnedObject();
});
if(gcHandle.IsAllocated)
gcHandle.Free();
string s = ASCIIEncoding.ASCII.GetString(bytes);
Console.WriteLine(s);
}
代表签名更改为:
public delegate IntPtr AllocateBufferDelegate(int bufsize);