LayoutKind.Sequential не используется, если в substruct есть LayoutKind.Explicit
При запуске этого кода:
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Runtime.InteropServices;
namespace StructLayoutTest
{
class Program
{
unsafe static void Main()
{
Console.WriteLine(IntPtr.Size);
Console.WriteLine();
Sequential s = new Sequential();
s.A = 2;
s.B = 3;
s.Bool = true;
s.Long = 6;
s.C.Int32a = 4;
s.C.Int32b = 5;
int* ptr = (int*)&s;
Console.WriteLine(ptr[0]);
Console.WriteLine(ptr[1]);
Console.WriteLine(ptr[2]);
Console.WriteLine(ptr[3]);
Console.WriteLine(ptr[4]);
Console.WriteLine(ptr[5]);
Console.WriteLine(ptr[6]);
Console.WriteLine(ptr[7]); //NB!
Console.WriteLine("Press any key");
Console.ReadKey();
}
[StructLayout(LayoutKind.Explicit)]
struct Explicit
{
[FieldOffset(0)]
public int Int32a;
[FieldOffset(4)]
public int Int32b;
}
[StructLayout(LayoutKind.Sequential, Pack = 4)]
struct Sequential
{
public int A;
public int B;
public bool Bool;
public long Long;
public Explicit C;
}
}
}
Я ожидаю, что этот вывод ОБА на x86 и x64:
4 или 8 (в зависимости от x86 или x64)
2
3
1
6
0
4
5
мусор
Что я получаю вместо этого на x86:
4
6
0
2
3
1
4
5
мусор
Что я получаю вместо этого на x64:
8
6
0
2
3
1
0
4
5
Больше:
- Проблема исчезает, когда я удаляю атрибуты LayoutKind.Explicit и FieldOffset.
- Проблема исчезает, когда я убираю поле Bool.
- Проблема исчезает, когда я удаляю поле Long.
- Обратите внимание, что на x64 кажется, что параметр атрибута Pack=4 тоже игнорируется?
Это относится к.Net3.5, а также.Net4.0
Мой вопрос: что мне не хватает? Или это ошибка?
Я нашел похожий вопрос:
Почему LayoutKind.Sequential работает по-другому, если структура содержит поле DateTime?
Но в моем случае компоновка меняется даже при изменении атрибута подструктуры без каких-либо изменений типов данных. Так что это не похоже на оптимизацию. Кроме того, я хотел бы отметить, что другой вопрос до сих пор остается без ответа.
В этом другом вопросе они упоминают, что компоновка соблюдается при использовании Marshalling. Я не проверял это сам, но мне интересно, почему компоновка не соблюдается для небезопасного кода, так как все соответствующие атрибуты, кажется, на месте? Упоминается ли где-нибудь в документации, что эти атрибуты игнорируются, если не выполнить маршалинг? Зачем?
Учитывая это, могу ли я ожидать, что LayoutKind.Explicit будет надежно работать с небезопасным кодом?
Кроме того, в документации упоминается мотив сохранения структур с ожидаемым расположением:
Чтобы уменьшить проблемы, связанные с компоновкой, связанные со значениями Auto, компиляторы C#, Visual Basic и C++ задают последовательную компоновку для типов значений.
Но этот мотив, очевидно, не относится к небезопасному коду?
1 ответ
Из статьи библиотеки MSDN для перечисления LayoutKind:
Точная позиция каждого члена объекта в неуправляемой памяти контролируется явно, в зависимости от значения поля StructLayoutAttribute.Pack. Каждый член должен использовать FieldOffsetAttribute, чтобы указать положение этого поля в типе.
Соответствующая фраза выделена, этого не происходит в этой программе, указатель по-прежнему очень сильно разыменовывает управляемую память.
И да, то, что вы видите, идентично тому, что происходит, когда структура содержит член типа DateTime, типа, к которому применено [StructLayout(LayoutKind.Auto)]. Код полевого маршаллера в CLR, который определяет макет, прилагает усилия для соблюдения LayoutKind.Sequential для управляемых структур. Но он быстро сдастся без визга, если столкнется с кем-то, кто вступает в конфликт с этой целью. Для этого достаточно структуры, которая сама по себе не является последовательной. Вы можете видеть, что это делается в источнике SSCLI20, src/clr/vm/fieldmarshaler.cpp, найдите fDisqualifyFromManagedSequential
Который заставит это переключиться на автоматическое расположение, то же самое правило расположения, которое применено к классам. Это переупорядочивает поля, чтобы минимизировать заполнение между членами. С чистым эффектом, что объем требуемой памяти меньше. После элемента "Bool" есть 7 байт заполнения, неиспользуемое пространство для выравнивания элемента "Long" с адресом, кратным 8. Конечно, это расточительно, но исправляет это, делая длинный элемент первым в макете.,
Поэтому вместо явного макета с /* offset - size */ annotated:
public int A; /* 0 - 4 */
public int B; /* 4 - 4 */
public bool Bool; /* 8 - 1 */
// padding /* 9 - 7 */
public long Long; /* 16 - 8 */
public Explicit C; /* 24 - 8 */
/* Total: 32 */
Это приходит с:
public long Long; /* 0 - 8 */
public int A; /* 8 - 4 */
public int B; /* 12 - 4 */
public bool Bool; /* 16 - 1 */
// padding /* 17 - 3 */
public Explicit C; /* 20 - 8 */
/* Total: 28 */
С легким сохранением 4 байта памяти. 64-битная компоновка требует дополнительного заполнения, чтобы гарантировать, что long все еще выровнен, когда он хранится в массиве. Все это в значительной степени недокументировано и может быть изменено, поэтому никогда не принимайте зависимость от структуры управляемой памяти. Только Marshal.StructureToPtr() может дать вам гарантию.