Выглядите массив структур из C ++ в C #? - PullRequest
5 голосов
/ 17 ноября 2009

В моем коде на C # я пытаюсь получить массив структур из устаревшей C ++ DLL (код, который я не могу изменить).

В этом коде C ++ структура определяется следующим образом:

struct MyStruct
{
    char* id;
    char* description;
};

Метод, который я вызываю (get_my_structures), возвращает указатель на массив структур MyStruct:

MyStruct* get_my_structures()
{
    ...
}

Существует еще один метод, который возвращает количество структур, поэтому я знаю, сколько структур возвращено.

В моем коде на C # я определил MyStruct следующим образом:

[StructLayout(LayoutKind.Sequential)]  
public class MyStruct
{
  [MarshalAsAttribute(UnmanagedType.LPStr)]    // <-- also tried without this
  private string _id;
  [MarshalAsAttribute(UnmanagedType.LPStr)]
  private string _description;
}

Подпись взаимодействия выглядит так:

[DllImport("legacy.dll", EntryPoint="get_my_structures")]
public static extern IntPtr GetMyStructures();

Наконец, код, который выбирает массив структур MyStruct, выглядит следующим образом:

int structuresCount = ...;
IntPtr myStructs = GetMyStructures();
int structSize = Marshal.SizeOf(typeof(MyStruct));    // <- returns 8 in my case
for (int i = 0; i < structuresCount; i++)
{
    IntPtr data = new IntPtr(myStructs.ToInt64() + structSize * i);
    MyStruct ms = (MyStruct) Marshal.PtrToStructure(data, typeof(MyStruct));
    ...
}

Беда в том, что только самая первая структура (одна с нулевым смещением) будет правильно распределена. Последующие имеют фиктивные значения в членах _id и _description. Значения не полностью уничтожены, или кажется, что это строки из других областей памяти. Сам код не вылетает.

Я убедился, что код C ++ в get_my_structures () действительно возвращает правильные данные. Данные не были случайно удалены или изменены во время или после разговора.

При просмотре в отладчике структура памяти возвращаемых данных в C ++ выглядит следующим образом:

0: id (char*)           <---- [MyStruct 1]
4: description (char*)
8: id (char*)           <---- [MyStruct 2]
12: description (char*)
16: id (char*)          <---- [MyStruct 3]
...

[Обновление 18/11/2009]

Вот как код на C ++ подготавливает эти структуры (фактический код гораздо уродливее, но это достаточно близкое приближение):

static char buffer[12345] = {0};
MyStruct* myStructs = (MyStruct*) &buffer;
for (int i = 0; i < structuresCount; i++)
{
    MyStruct* ms = <some other permanent address where the struct is>;
    myStructs[i].id = (char*) ms->id;
    myStructs[i].description = (char*) ms->description;
}
return myStructs;

По общему признанию, приведенный выше код выполняет некрасивое преобразование и копирует необработанные указатели, но, похоже, все же делает это правильно. По крайней мере, это то, что я вижу в отладчике: вышеупомянутый (статический) буфер содержит все эти голые указатели char *, хранящиеся один за другим, и они указывают на допустимые (нелокальные) расположения в памяти.

Пример Павла показывает, что это действительно единственное место, где все может пойти не так. Я попытаюсь проанализировать, что происходит с теми «конечными» местами, где действительно находятся строки, а не местами, где хранятся указатели.

Ответы [ 5 ]

3 голосов
/ 17 ноября 2009

Я не могу воспроизвести вашу проблему, из-за чего я подозреваю, что она действительно на стороне C ++. Вот полный исходный код моей попытки.

dll.cpp - компилировать с cl.exe /LD:

extern "C" {

struct MyStruct
{
    char* id;
    char* description;
};

__declspec(dllexport)
MyStruct* __stdcall get_my_structures()
{
    static MyStruct a[] =
    {
        { "id1", "desc1" },
        { "id2", "desc2" },
        { "id3", "desc3" }
    };
    return a;

}

}

test.cs - компилировать с csc.exe /platform:x86:

using System;
using System.Runtime.InteropServices;


[StructLayout(LayoutKind.Sequential)]  
public class MyStruct
{
  [MarshalAsAttribute(UnmanagedType.LPStr)]
  public string _id;
  [MarshalAsAttribute(UnmanagedType.LPStr)]
  public string _description;
}


class Program
{
    [DllImport("dll")]
    static extern IntPtr get_my_structures();

    static void Main()
    {
        int structSize = Marshal.SizeOf(typeof(MyStruct));
        Console.WriteLine(structSize);

        IntPtr myStructs = get_my_structures();
        for (int i = 0; i < 3; ++i)
        {
            IntPtr data = new IntPtr(myStructs.ToInt64() + structSize * i);
            MyStruct ms = (MyStruct) Marshal.PtrToStructure(data, typeof(MyStruct));

            Console.WriteLine();
            Console.WriteLine(ms._id);
            Console.WriteLine(ms._description);
        }
    }
}

Это правильно распечатывает все 3 структуры.

Можете ли вы показать свой код C ++, который заполняет структуры? Тот факт, что вы можете вызывать его из C ++ напрямую и получать правильные результаты, не обязательно означает, что это правильно. Например, вы можете возвращать указатель на структуру, размещенную в стеке. Тогда при выполнении прямого вызова вы получите технически неверный указатель, но данные, скорее всего, останутся сохраненными. При выполнении маршалинга P / Invoke стек может быть перезаписан структурами данных P / Invoke в тот момент, когда он пытается прочитать значения оттуда.

1 голос
/ 17 ноября 2009

Я бы изменил структуру. Вместо строк и т. Д. Используйте IntPtr:

[StructLayout(LayoutKind.Sequential)]  
public class MyStruct
{
  private IntPtr _id;
  private IntPtr _description;
}

Тогда каждое значение массива C # можно вручную маршалировать в строку, используя Marshal.PtrToString с учетом кодировки и т. Д.

0 голосов
/ 17 ноября 2009

Я обычно заканчиваю работать над этим методом проб и ошибок. Убедитесь, что в вашем StructLayout установлено свойство CharSet, и я бы попробовал UnmanagedType.LPTStr, который, кажется, лучше работает для char *, хотя я не уверен почему.

[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Auto)]  
public class MyStruct
{
    [MarshalAsAttribute(UnmanagedType.LPTStr)]
    private string _id;
    [MarshalAsAttribute(UnmanagedType.LPTStr)]
    private string _description;
}
0 голосов
/ 17 ноября 2009

Я думаю, кроме того, что в дополнение к ответам, вам также необходимо указать длину, т.е. [MarshalAsAttribute (UnmanagedType.LPTStr), SizeConst =, ArraySubType = System.Runtime.InteropServices.UnmanagedType.AnsiBStr)]

Это метод проб и ошибок, позволяющий сделать это правильно, а также еще одна вещь, которую следует учитывать при некоторых вызовах WinAPI, которые ожидают строковый параметр, обычно параметр ref, возможно, стоит попробовать класс StringBuilder. . Ничего другого не приходит на ум, кроме упомянутых мною здесь моментов ... Надеюсь, это поможет, Том

0 голосов
/ 17 ноября 2009

Вы должны использовать UnmanagedType.LPTStr для символа *. Также StringBuilder рекомендуется для неконстантного символа *: И спецификация CharSet:

[StructLayout(LayoutKind.Sequential, Charset = CharSet.Auto)]  
public class MyStruct
{
  [MarshalAsAttribute(UnmanagedType.LPTStr)]
  private StringBuilder _id;
  [MarshalAsAttribute(UnmanagedType.LPTStr)]
  private StringBuilder _description;
}

Что касается объявления DllImport, вы пробовали

[DllImport("legacy.dll", EntryPoint="get_my_structures")]
public static extern MarshalAs(UnmanagedType.LPArray) MyStruct[] GetMyStructures();

Кроме того, если предыдущий не работает, оставьте его в IntPtr и попробуйте Mashal возвращаемых структур следующим образом:

for (int i = 0; i < structuresCount; i++)
{
    MyStruct ms = (MyStruct) Marshal.PtrToStructure(myStructs, typeof(MyStruct));
    ...
    myStructs += Marshal.SizeOf(ms);
}
...