如何从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");
}

如何从PInvoke本机回调返回StringBuilder或其他字符串缓冲区

我有一个理论来解释为什么会发生这种情况。我怀疑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);