Массив указателей в сборке C++/CLI MSIL

Я пытаюсь обернуть некоторый устаревший код C для использования с C#, работающим на .NET Core. Я использую приведенный здесь подход для создания оболочки C++, которая компилируется в чистый MSIL. Это хорошо работает для простых функций, но я обнаружил, что если мой код когда-либо использует указатели на указатели или массивы указателей, он рухнет с нарушением памяти. Часто вылетает Visual Studio и приходится все перезагружать, что утомительно.

Например, следующий код вызовет сбои:

public ref class example
    {
    public:

        static void test() {
            Console::WriteLine("\nTesting pointers.");

            double a[5] = {5,6,7,8,9}; //Array.
            double *b = a; //Pointer to first element in array.

            Console::WriteLine("\nTesting bare pointers.");
            Console::WriteLine(a[0]); //Prints 5.
            Console::WriteLine(b[0]); //Prints 5.

            Console::WriteLine("\nTesting pointer-to-pointer.");
            double **c = &b;
            Console::WriteLine(c == &b); //Prints true.
            Console::WriteLine(b[0]); //Works, prints 5.
            Console::WriteLine(**c); //Crashes with memory access violation.

            Console::WriteLine("\nTesting array of pointers.");
            double* d[1];
            d[0] = b;
            Console::WriteLine(d[0] == b); //Prints false???
            Console::WriteLine(b[0]); //Works, prints 5.
            Console::WriteLine(d[0][0]); //Crashes with memory access violation.

            Console::WriteLine("\nTesting CLI array of pointers.");
            cli::array<double*> ^e = gcnew cli::array<double*> (5);
            e[0] = b;
            Console::WriteLine(e[0] == b); //Prints false???
            Console::WriteLine(b[0]); //Works, prints 5.
            Console::WriteLine(e[0][0]); //Crashes with memory access violation.
        }
}

Обратите внимание, что простое использование указателей не вызывает никаких проблем. Это только тогда, когда есть дополнительный уровень косвенности.

Если я добавляю код в консольное приложение CLR C++, оно работает точно так, как ожидалось, и не дает сбоев. Сбой происходит только при компиляции кода в сборку MSIL с clr:pure и запуске из основного приложения .NET.

Что может происходить?

Обновление 1. Вот файлы Visual Studio: https://app.box.com/s/xejfm4s46r9hs0inted2kzhkh9qzmjpb Это два проекта. Сборка MSIL называется library, а CoreApp — это консольное приложение C#, которое вызывает библиотеку. Предупреждение, при запуске Visual Studio может произойти сбой.

Обновление 2: я тоже это заметил:

        double a[5] = { 5,6,7,8,9 };
        double* d[1];
        d[0] = a;
        Console::WriteLine(d[0] == a); //Prints true.
        Console::WriteLine(IntPtr(a)); //Prints a number.
        Console::WriteLine(IntPtr(d[0])); //Prints a completely different number.

person Imbue    schedule 04.07.2018    source источник
comment
PITA для сборки, но работает просто отлично, как и ожидалось. Просто это совсем не особенное.   -  person Hans Passant    schedule 05.07.2018
comment
@HansPassant Вы построили его как чистую сборку MSIL? А затем звонить из приложения .NET Core? Это единственные условия, при которых происходит сбой. Это не приведет к сбою, если оно создано или запущено из настольного приложения (.NET Framework).   -  person Imbue    schedule 05.07.2018
comment
github.com/mono/CppSharp   -  person Lex Li    schedule 05.07.2018
comment
Конечно. Если вы хотите, чтобы кто-то посмотрел на ваше решение, вам придется его где-нибудь опубликовать.   -  person Hans Passant    schedule 05.07.2018
comment
@HansPassant Вот код. Имеет два проекта. Библиотека — это приведенный выше код в интерфейсе командной строки C++, сконфигурированный для компиляции как чистый MSIL. CoreApp — это консольное приложение C#, которое вызывает библиотеку. Пробовал на паре компов. Он дает сбой и обычно берет с собой Visual Studio. app.box.com/s/xejfm4s46r9hs0inted2kzhkh9qzmjpb Спасибо за внимание!   -  person Imbue    schedule 05.07.2018


Ответы (1)


Это похоже на проблему в сгенерированном IL для метода test. В момент сбоя мы читаем **c, а c — это локальный номер 5.

IL_00a5  11 05             ldloc.s      0x5
IL_00a7  4a                ldind.i4    
IL_00a8  4f                ldind.r8    
IL_00a9  28 11 00 00 0a    call         0xA000011

Итак, здесь мы видим, что IL говорит загрузить значение c, затем загрузить 4-байтовое целое число со знаком, затем обработать это целое число как указатель и загрузить 8-байтовый реальный тип (double).

На 64-битной платформе указатели должны быть либо нейтральными по размеру, либо 64-битными. Таким образом, ldind.i4 проблематичен, поскольку базовый адрес составляет 8 байтов. А поскольку IL указывает чтение только 4 байтов, jit должен расширить результат, чтобы получить 8-байтовое значение. Здесь он выбирает расширение подписи.

library.h @ 27:
00007ffd`b0cf2119 488b45a8        mov     rax,qword ptr [rbp-58h]
00007ffd`b0cf211d 8b00            mov     eax,dword ptr [rax]
00007ffd`b0cf211f 4863c0          movsxd  rax,eax   // **** sign extend ****
>>> 00007ffd`b0cf2122 c4e17b1000      vmovsd  xmm0,qword ptr [rax]
00007ffd`b0cf2127 e854f6ffff      call    System.Console.WriteLine(Double) (00007ffd`b0cf1780)

Очевидно, вам повезло, когда вы работаете на полной платформе, поскольку адрес массива мал и умещается в 31 бит или меньше, поэтому чтение 4 байтов, а затем расширение знака до 8 байтов по-прежнему дает правильный адрес. Но на Core этого нет, поэтому приложение там вылетает.

Похоже, вы создали свою библиотеку, используя цель Win32. Если вы пересоберете его с целью x64, IL будет использовать 64-битную загрузку для *c:

IL_00ab:  ldloc.s    V_5
IL_00ad:  ldind.i8
IL_00ae:  ldind.r8
IL_00af:  call       void [mscorlib]System.Console::WriteLine(float64)

и приложение работает нормально.

Похоже, это особенность C++/CLI — создаваемые ею двоичные файлы неявно зависят от архитектуры даже в чистом режиме. Только /clr:safe может создавать архитектурно-независимые сборки, и вы не можете использовать это с этим кодом, поскольку он содержит непроверяемые конструкции, такие как указатели.

Также обратите внимание, что не все функции C++/CLI поддерживаются в .Net Core 2.x. Этот конкретный пример избегает неподдерживаемых битов, но более сложные могут и не использоваться.

person Andy Ayers    schedule 08.07.2018
comment
Спасибо за Ваш ответ. Может быть, вы могли бы прояснить для меня момент.? Один голый указатель работает нормально. Только когда задействован второй уровень косвенности, дела идут плохо. Это почему? - person Imbue; 13.07.2018
comment
Поскольку b является локальным, IL использует ldloc для получения его значения, а поскольку b имеет тип float*, это всегда дает указатель правильного размера. Точно так же при загрузке c IL получает указатель правильного размера, но при загрузке *c IL должен сообщить JIT, какой размер использовать, и здесь он всегда указывает 4 байта для сборки x86 и 8 для сборки x64. Иными словами, иногда размер загружаемого значения является неявным (задается либо кодом операции, скажем, ldloca, либо с помощью подписи), а иногда — явным. В явном случае производитель IL должен указать правильный размер. - person Andy Ayers; 14.07.2018