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

时间:2012-02-21 13:43:11

标签: 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);
    }
}

原生标题

#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");
}

2 个答案:

答案 0 :(得分:5)

我有一个关于为什么会发生这种情况的理论。我怀疑StringBuilder的编组涉及复制数据,将其传递给P / Invoke调用,然后复制回StringBuilderI couldn't actually verify this虽然。{/ p>

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

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

当您传递一个返回StringBuilder的委托时,情况并非如此。在这种情况下,.NET需要创建一个包装器,将int => StringBuilder函数转换为int => char*函数。这个包装器将创建char*缓冲区并填充它,但显然无法复制任何更改。在接受委托的函数返回后,它也无法执行此操作:它还为时尚早!

事实上,没有明显的地方可以进行反向复制。

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


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

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

答案 1 :(得分:1)

除了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);