C# вызывает функцию C, которая возвращает структуру с массивом символов фиксированного размера

Итак, было много вариантов этого вопроса, и, просмотрев несколько, я все еще не могу понять это.

Это код C:

typedef struct
{
unsigned long Identifier;
char Name[128];
} Frame;

Frame GetFrame(int index);

Это код С#:

struct Frame
{
    public ulong Identifier;
    [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.I1, SizeConst = 128)]
    public char[] Name;
}

[DllImport("XNETDB.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern Frame GetFrame(int index);

Это последняя попытка, которую я пробовал на С#, и она кажется довольно логичной, но я получаю сообщение об ошибке «Подпись метода несовместима с PInvoke». Итак, я немного потерялся в том, что попробовать дальше. Любая помощь приветствуется.

Спасибо, Кевин

Обновлено Кевин добавил это как правку в мой ответ

Вместо этого я должен изменить свой код C:

void GetFrame(int index, Frame * f);

и используйте вместо этого для С#:

struct Frame
{
    public uint Identifier;
    [MarshalAsAttribute(UnmanagedType.ByValTStr, SizeConst = 128)]
    public string Name;
}

[DllImport("XNETDB.dll", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
private static extern void GetFrame(int index, ref Frame f);

person kevin.key    schedule 25.04.2012    source источник
comment
Видели ли вы эту социальную сеть. msdn.microsoft.com/Forums/en-AU/csharplanguage/thread/ ?   -  person MilkyWayJoe    schedule 25.04.2012
comment
Определение функции существует. Приведенный код C взят только из заголовочного файла.   -  person kevin.key    schedule 25.04.2012
comment
Я видел это и пробовал private static extern IntPtr GetFrame(int index); но вызов этого вызывает ошибку Попытка чтения или записи защищенной памяти.   -  person kevin.key    schedule 25.04.2012


Ответы (3)


Проблема в том, что нативная функция возвращает непреобразуемый тип в качестве возвращаемого значения.

http://msdn.microsoft.com/en-us/library/ef4c3t39.aspx

P/Invoke не может иметь непреобразуемые типы в качестве возвращаемого значения.

Вы не можете p/Invoke этого метода. [EDIT Это действительно возможно, см. ответ JaredPar]

Возвращать 132 байта по значению — плохая идея. Если бы этот нативный код был вашим, я бы его поправил. Вы можете исправить это, выделив 132 байта и вернув указатель. Затем добавьте метод FreeFrame, чтобы освободить эту память. Теперь его можно p/Invoked.

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

person Tergiver    schedule 25.04.2012
comment
+1, совсем забыл о типе возвращаемого значения, должно быть преобразовываемое правило. Согласитесь, что если у вас есть контроль, лучше предоставить более легкий слой. Если вы этого не сделаете, я добавил работоспособное (но уродливое) решение. - person JaredPar; 25.04.2012
comment
@ kevin.key Я бы пометил ответ JaredPar как ответ сейчас, поскольку он работает напрямую, если вы не можете изменить собственный код. - person Tergiver; 25.04.2012
comment
Я отметил это как ответ. Ключом была передача структуры по ссылке в C# и изменение функции C для передачи структуры через параметр-указатель. - person kevin.key; 26.04.2012

Есть две проблемы с выбранной вами подписью PInvoke.

Первое легко исправить. У вас неправильный перевод unsigned long. В C unsigned long обычно составляет всего 4 байта. Вы выбрали тип С# long размером 8 байт. Изменение кода C# для использования uint исправит это.

Второй немного сложнее. Как указал Тергивер, CLR Marshaller поддерживает структуру в позиции возврата только в том случае, если она является преобразовываемой. Blittable — это причудливый способ сказать, что он имеет точно такое же представление памяти в нативном и управляемом коде. Выбранное вами определение структуры не является преобразовываемым, поскольку оно имеет вложенный массив.

Это можно обойти, если вы помните, что PInvoke — очень простой процесс. CLR Marshaller действительно нужно, чтобы вы ответили на 2 вопроса с подписью ваших типов и методов pinvoke.

  • Сколько байт я копирую?
  • В каком направлении им нужно идти?

В этом случае количество байтов равно sizeof(unsigned long) + 128 == 132. Итак, все, что нам нужно сделать, это создать управляемый тип, способный к преобразованию и имеющий размер 132 байта. Самый простой способ сделать это — определить большой двоичный объект для обработки части массива.

[StructLayout(LayoutKind.Sequential, Size = 128)]
struct Blob
{
   // Intentionally left empty. It's just a blob
}

Это структура без членов, которая будет отображаться для маршаллера как имеющая размер 128 байт (и в качестве бонуса она может быть преобразована!). Теперь мы можем легко определить структуру Frame как комбинацию uint и этого типа.

struct Frame
{
    public int Identifier;
    public Blob NameBlob;
    ...
}

Теперь у нас есть преобразуемый тип с размером, который маршаллер увидит как 132 байта. Это означает, что он будет отлично работать с определенной вами подписью GetFrame.

Единственная оставшаяся часть — дать вам доступ к фактическому char[] для имени. Это немного сложно, но может быть решено с помощью магии маршала.

public string GetName()
{
    IntPtr ptr = IntPtr.Zero;
    try
    {
        ptr = Marshal.AllocHGlobal(128);
        Marshal.StructureToPtr(NameBlob, ptr, false);
        return Marshal.PtrToStringAnsi(ptr, 128);
    }
    finally
    {
        if (ptr != IntPtr.Zero) 
        {
            Marshal.FreeHGlobal(ptr);
        }
    }
}

Примечание. Я не могу комментировать часть соглашения о вызовах, потому что я не знаком с GetFrame API, но это то, что я обязательно проверю.

person JaredPar    schedule 25.04.2012
comment
Я не вижу, чтобы этот тип был более или менее совместим с P/Invoke на основе этого изменения, хотя это, безусловно, проблема переноса, которую следует исправить. - person Ben Voigt; 25.04.2012
comment
@BenVoigt это может иметь большое значение. В исходной подписи маршаллер предположит, что char[] начинается со смещения 8 от начала структуры по сравнению с реальным значением 4. В результате маршаллер видит 4 байта, которых на самом деле нет. Он будет записывать эти 4 байта при маршаллинге в родной и читать эти 4 байта при обратном маршаллинге из родного. 4 байта по сути являются мусором и, следовательно, приводят к неопределенному поведению. - person JaredPar; 25.04.2012
comment
Конечно, это влияет на правильность. Но это не вызовет ошибки компиляции. И ulong, и uint являются преобразовываемыми, p/invoke обрабатывает их одинаково (хотя и в разной степени). - person Ben Voigt; 25.04.2012
comment
@BenVoigt Я не думаю, что пользователь получает ошибку компилятора в любом случае. Я считаю, что это ошибка времени выполнения. Компилятор C# не использует рабочий PInvoke ни в одном из своих сообщений. - person JaredPar; 25.04.2012
comment
Это ошибка времени выполнения, которую я получаю. Изменение ulong на uint по-прежнему приводит к тому, что подпись метода исключения не совместима с PInvoke. Я использую другую функцию из той же DLL, которая ожидает unsigned long, и импорт прототипа с помощью ulong или uint успешно работает в любом случае. - person kevin.key; 25.04.2012
comment
@ kevin.key всего минуту, забыл, что возвраты Pinvoke могут быть только преобразовываемыми значениями. Будет решение для вас всего за несколько минут - person JaredPar; 25.04.2012
comment
@JaredPar Отличная работа! Я не думал о том, чтобы создать полностью преобразовываемый тип, а затем преобразовать его. p.s. вы можете использовать Marshal.PtrToStringAnsi вместо перебора каждого символа. - person Tergiver; 25.04.2012
comment
@JaredPar Я подозреваю, что вы знаете обо всем этом намного больше, чем я, но я не вижу причин, по которым UnmanagedType.ByValTStr здесь не работает. Что мне не хватает? - person David Heffernan; 26.04.2012
comment
@DavidHeffernan ключ в том, что он не преобразовывается. У маршаллера есть правило, согласно которому тип взаимодействия, отображаемый в качестве возврата метода, должен быть преобразовываемым, иначе будет выдано исключение. Управляемый string несовместим с собственным кодом, поэтому его включение делает его недействительным. - person JaredPar; 26.04.2012
comment
@JaredPar Спасибо. Я прочитал документацию по непреобразуемым и непреобразуемым типам и понял. Что меня зацепило, так это то, что мой тестовый код использовал параметр ref, потому что я не доверяю своему родному компилятору (Delphi) обрабатывать возвращаемые значения так же, как инструменты MS. Я знаю, что это отклонение от темы, но знаете ли вы, почему возвращаемые значения выделены для особой обработки? Почему они должны быть преобразовываемыми, а параметры не должны быть преобразовываемыми? Ты знаешь? - person David Heffernan; 26.04.2012
comment
@DavidHeffernan Я не знаю, почему у них есть это ограничение, и я неоднократно сталкивался с ним (включая первую версию этого ответа) - person JaredPar; 26.04.2012
comment
@JaredPar Ах, прочитав вашу первую версию, я уже не так расстроен из-за своих довольно неумелых возни! Кстати, большое спасибо за ловкий трюк в вашем окончательном ответе! - person David Heffernan; 26.04.2012

Другой вариант для JaredPar — использовать функцию буфера фиксированного размера C#. Однако это требует, чтобы вы включили настройку, чтобы разрешить небезопасный код, но это позволяет избежать двух структур.

class Program
{
    private const int SIZE = 128;

    unsafe public struct Frame
    {
        public uint Identifier;
        public fixed byte Name[SIZE];
    }

    [DllImport("PinvokeTest2.DLL", CallingConvention = CallingConvention.Cdecl, CharSet = CharSet.Ansi)]
    private static extern Frame GetFrame(int index);

    static unsafe string GetNameFromFrame(Frame frame)
    {
        //Option 1: Use if the string in the buffer is always null terminated
        //return Marshal.PtrToStringAnsi(new IntPtr(frame.Name));

        //Option 2: Use if the string might not be null terminated for any reason,
        //like if were 128 non-null characters, or the buffer has corrupt data.

        return Marshal.PtrToStringAnsi(new IntPtr(frame.Name), SIZE).Split('\0')[0];
    }

    static void Main()
    {
        Frame a = GetFrame(0);
        Console.WriteLine(GetNameFromFrame(a));
    }
}
person Kevin Cathcart    schedule 25.04.2012
comment
Хороший! Вы можете использовать перегрузку PtrToStringAnsi с размером 128, чтобы она не переполнялась, если нет завершающего символа NULL. - person Tergiver; 26.04.2012
comment
Если есть вероятность того, что нулевого символа не будет, вам следует использовать второй вариант, который я показываю, который действительно использует эту перегрузку. К сожалению, AFAICT эта перегрузка всегда будет создавать строку из 128 символов вместо того, чтобы останавливаться на первом NULL. Вот почему я добавил разделение на нулевой символ и выбрал первый результат. - person Kevin Cathcart; 26.04.2012
comment
Вы правы, результирующая длина строки будет размером, который вы передаете. - person Tergiver; 26.04.2012