Как я могу вернуть StringBuilder или другой строковый буфер из нативного обратного вызова PInvoke - PullRequest
2 голосов
/ 21 февраля 2012

Я бы хотел чистый способ увеличить размер StringBuilder (), как требуется для заполнения нативным кодом, метод обратного вызова, представленный ниже, кажется чистым, но каким-то образом мы получаем копию буфера вместо фактического буфера - I 'Я заинтересован в объяснениях и решениях (желательно придерживаться распределения типа обратного вызова, поскольку было бы хорошо и чисто, если бы только это можно было заставить работать).

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 ]

5 голосов
/ 21 февраля 2012

У меня есть теория, почему это происходит.Я подозреваю, что сортировка 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.

Это можно превратить в более удобную функцию, которую можно просто вызывать из управляемого мира, не беспокоясь о буферах.

1 голос
/ 22 февраля 2012

В дополнение к информации, предоставленной 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);
...