Это проблема, которая возникает именно на ARM, а не на x86 или x64. Пользователь сообщил мне об этой проблеме, и я смог воспроизвести ее с помощью UWP на Raspberry Pi 2 через Windows IoT. Я уже сталкивался с подобными проблемами с несовпадающими соглашениями о вызовах, но я указываю Cdecl в объявлении P / Invoke и пробовал явно добавить __cdecl на нативной стороне с теми же результатами. Вот некоторая информация:
Объявление P / Invoke ( ссылка ):
[DllImport(Constants.DllName, CallingConvention = CallingConvention.Cdecl)]
public static extern FLSliceResult FLEncoder_Finish(FLEncoder* encoder, FLError* outError);
Структуры C # ( ссылка ):
internal unsafe partial struct FLSliceResult
{
public void* buf;
private UIntPtr _size;
public ulong size
{
get {
return _size.ToUInt64();
}
set {
_size = (UIntPtr)value;
}
}
}
internal enum FLError
{
NoError = 0,
MemoryError,
OutOfRange,
InvalidData,
EncodeError,
JSONError,
UnknownValue,
InternalError,
NotFound,
SharedKeysStateError,
}
internal unsafe struct FLEncoder
{
}
Функция в заголовке C ( ссылка )
FLSliceResult FLEncoder_Finish(FLEncoder, FLError*);
FLSliceResult может вызывать некоторые проблемы, потому что он возвращается по значению и имеет некоторые вещи C ++ на собственной стороне?
Структуры на собственной стороне содержат фактическую информацию, но для C API FLEncoder определяется как непрозрачный указатель . При вызове описанного выше метода на x86 и x64 все работает плавно, но на ARM я наблюдаю следующее. Адрес первого аргумента - это адрес ВТОРОГО аргумента, а второй аргумент имеет значение null (например, когда я регистрирую адреса на стороне C #, я получаю, например, 0x054f59b8 и 0x0583f3bc, но затем на собственной стороне аргументы 0x0583f3bc и 0x00000000). Что могло вызвать такого рода проблемы с выходом из строя? Есть ли у кого-нибудь идеи, потому что я в тупике ...
Вот код, который я запускаю для воспроизведения:
unsafe {
var enc = Native.FLEncoder_New();
Native.FLEncoder_BeginDict(enc, 1);
Native.FLEncoder_WriteKey(enc, "answer");
Native.FLEncoder_WriteInt(enc, 42);
Native.FLEncoder_EndDict(enc);
FLError err;
NativeRaw.FLEncoder_Finish(enc, &err);
Native.FLEncoder_Free(enc);
}
Прекрасно работает приложение C ++ со следующим:
auto enc = FLEncoder_New();
FLEncoder_BeginDict(enc, 1);
FLEncoder_WriteKey(enc, FLSTR("answer"));
FLEncoder_WriteInt(enc, 42);
FLEncoder_EndDict(enc);
FLError err;
auto result = FLEncoder_Finish(enc, &err);
FLEncoder_Free(enc);
Эта логика может вызвать сбой с последней сборкой разработчикано, к сожалению, я еще не понял, как надежно предоставить собственные символы отладки через Nuget, чтобы их можно было пройти по шагам (кажется, что это делает только сборка всего из источника ...), поэтому отладка немного неудобна, потому что оба родных и должны быть созданы управляемые компоненты. Я открыт для предложений о том, как сделать это проще, если кто-то захочет попробовать. Но если кто-то сталкивался с этим раньше или имеет какие-либо идеи о том, почему это происходит, пожалуйста, добавьте ответ, спасибо! Конечно, если кому-то нужен случай воспроизведения (либо простой в сборке, который не обеспечивает пошаговое выполнение исходного кода, либо сложный для сборки, который делает), оставьте комментарий, но я не хочу проходить процесс его создания. если никто не собирается его использовать (я не уверен, насколько популярно запускать Windows на реальном ARM)
РЕДАКТИРОВАТЬ Интересное обновление: если я «подделываю» подпись в C # и удаляю второй параметр, то первый приходит через OK.
РЕДАКТИРОВАТЬ 2 Второе интересное обновление: если я изменю определение размера в C # FLSliceResult с UIntPtr
на, ulong
тогда аргументы будут введены правильно ... что не имеет смысла, поскольку size_t
на ARM должно быть unsigned int.
РЕДАКТИРОВАТЬ 3 Добавление [StructLayout(LayoutKind.Sequential, Size = 12)]
к определению в C # также делает эту работу, но ПОЧЕМУ? sizeof (FLSliceResult) в C / C ++ для этой архитектуры возвращает 8, как и должно. Установка того же размера в C # вызывает сбой, но установка 12 заставляет его работать.
РЕДАКТИРОВАТЬ 4 Я минимизировал тестовый пример, чтобы я мог также написать тестовый пример на C ++. В C # UWP это не удается, но в C ++ UWP это удается.
РЕДАКТИРОВАТЬ 5 Вот дизассемблированные инструкции для C ++ и C # для сравнения (хотя C # я не уверен, сколько нужно взять, поэтому я ошибся в том, что взял слишком много)
РЕДАКТИРОВАТЬ 6 Дальнейший анализ показывает, что во время «хорошего» прогона, когда я лгу и говорю, что структура состоит из 12 байтов на C #, возвращаемое значение передается в регистр r0, а два других аргумента приходят через r1, r2. Однако при неудачном запуске это смещается, так что два аргумента поступают через r0, r1, а возвращаемое значение находится где-то еще (указатель стека?)
РЕДАКТИРОВАТЬ 7 Я проконсультировался со стандартом вызова процедур для архитектуры ARM . Я нашел эту цитату: «Составной тип размером более 4 байтов или размер которого не может быть определен статически как вызывающим, так и вызываемым пользователем, сохраняется в памяти по адресу, переданному в качестве дополнительного аргумента при вызове функции (§5.5, правило A .4). Память, которая будет использоваться для результата, может быть изменена в любой момент во время вызова функции ". Это означает, что переход в r0 является правильным поведением, поскольку дополнительный аргумент подразумевает первый (поскольку в соглашении о вызовах C нет способа указать количество аргументов). Интересно, путает ли CLR это с другим правилом фундаментального 64-битные типы данных: «Фундаментальный тип данных размером с двойное слово (например, длинные длинные, двойные и 64-битные контейнерные векторы) возвращается в r0 и r1».
РЕДАКТИРОВАТЬ 8 Хорошо, есть много свидетельств, указывающих на то, что CLR делает здесь неправильные вещи, поэтому я отправил отчет об ошибке . Надеюсь, кто-то заметит это между всеми автоматическими ботами, публикующими проблемы в этом репо: -S.