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

dic*_*ice 2 c# pinvoke stringbuilder

我想要一个干净的方法来增加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);
    }
}
Run Code Online (Sandbox Code Playgroud)

本地标题

#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);
Run Code Online (Sandbox Code Playgroud)

本机代码

#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");
}
Run Code Online (Sandbox Code Playgroud)

Rom*_*kov 5

我有一个理论为什么会发生这种情况.我怀疑编组StringBuilder涉及制作数据的副本,将其传递给P/Invoke调用,然后复制回StringBuilder.我实际上无法验证这一点.

对此的唯一替代方法是首先要求展StringBuilder平(它在内部是一个链接的列表char[])和char[]固定的,即使这样,这只能用于编组指针到Unicode-chars字符串,但不能ANSI或COM字符串.

因此,当您传递一个StringBuilder参数时,.NET有一个显而易见的地方可以复制任何更改:在P/Invoke返回之后.

当你传递一个委托返回一个时,情况也是如此StringBuilder.在这种情况下,.NET需要创建一个将int => StringBuilder函数转换为函数的包装器int => char*.这个包装器将创建char*缓冲区并填充它,但显然无法复制任何更改.这还不能说该功能后,做到这一点需要委托回报:它仍然为时过早!

实际上,在反向拷贝可能发生的地方根本没有明显的位置.

所以我的猜测是发生了这样的事情:当编组一个StringBuilder返回委托时,.NET只能执行单向转换,因此你所做的任何更改都不会反映在StringBuilder.这比完全无法组织这样的代表要好一些.


至于解决方案:我建议首先询问本机代码缓冲区需要多大,然后在第二次调用中传递适当大小的缓冲区.或者,如果您需要更好的性能,请猜测足够大的缓冲区,但允许本机方法进行通信,以确保需要更多空间.这样,大多数调用只涉及一个P/Invoke转换.

这可以包装成一个更方便的功能,您可以从托管世界调用,而无需担心缓冲区.