How can I return a StringBuilder or other string buffer from a PInvoke native callback

耗尽温柔 提交于 2019-12-01 00:31:03
Roman Starkov

I have a theory for why this happens. I suspect that the marshalling of StringBuilder involves making a copy of the data, passing it to the P/Invoke call, and then copying back into the StringBuilder. I couldn't actually verify this though.

The only alternative to this would require the StringBuilder to be flattened first (it is internally a linked list of char[]'s), and the char[] pinned, and even then this would only work for marshalling to pointer-to-Unicode-chars strings, but not to ANSI or COM strings.

Thus, when you pass a StringBuilder as an argument, there's an obvious place for .NET to copy any changes back: right after the P/Invoke returns.

The same isn't true for when you pass a delegate returning a StringBuilder. In this case .NET needs to create a wrapper which converts an int => StringBuilder function into an int => char* function. This wrapper will create the char* buffer and populate it, but obviously can't copy any changes back yet. It also can't do this after the function that takes the delegate returns: it's still too early!

In fact, there is no obvious place at all where the reverse copy could occur.

So my guess is that this is what happens: when marshalling a StringBuilder-returning delegate, .NET can only perform a one-way conversion, hence any changes you make aren't reflected in the StringBuilder. This is slightly better than being completely unable to marshal such delegates.


As for solutions: I would recommend first asking the native code how large the buffer needs to be, and then passing a buffer of the appropriate size in a second call. Or, if you need better performance, guess a large enough buffer, but allow the native method to communicate that more space is required. This way most calls would involve only one P/Invoke transition.

This can be wrapped into a more convenient function that you can just call from the managed world without worrying about buffers.

In addition to the input provided by romkyns I will share the minimal changes solution I came up with. If anyone uses this be careful of your encodings!

the principal modification is:

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

with the delegate signature changeing to:

public delegate IntPtr AllocateBufferDelegate(int bufsize);
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!