Поведение .NET LayoutKind.explicit для поля, которое само является структурой

Вопрос

Я попытался создать структуру (SA), используя [StructLayout(LayoutKind.Explicit)], в которой было поле, являющееся другим struct (SB).

Во-первых: я был удивлен, что мне разрешили объявить эту другую структуру без [StructLayout(LayoutKind.Explicit)], тогда как в SA все поля должны иметь [FieldOffset(0)], иначе компилятор будет кричать. Это не имеет особого смысла.

  • Является ли это лазейкой в ​​предупреждениях/ошибках компилятора?

Во-вторых: кажется, что все поля ссылок (object) в SB перемещены в начало SB.

  • Где-нибудь описано такое поведение?
  • Это зависит от реализации?
  • Определено ли где-нибудь, что это зависит от реализации? :)

Примечание. Я не собираюсь использовать это в рабочем коде. Я задаю этот вопрос в основном из любопытства.

Эксперимент

// No object fields in SB
// Gives the following layout (deduced from experimentation with the C# debugger):

// | f0 | f4 and i | f8 and j | f12 and k | f16 |

[StructLayout(LayoutKind.Explicit)]
struct SA {
    [FieldOffset(0)] int f0;
    [FieldOffset(4)] SB sb;
    [FieldOffset(4)] int f4;
    [FieldOffset(8)] int f8;
    [FieldOffset(12)] int f12;
    [FieldOffset(16)] int f16;
}
struct SB { int i; int j; int k; }

// One object field in SB
// Gives the following layout:

// | f0 | f4 and o1 | f8 and i | f12 and j | f16 and k |

// If I add an `object` field after `j` in `SB`, i *have* to convert
// `f4` to `object`, otherwise I get a `TypeLoadException`.
// No other field will do.

[StructLayout(LayoutKind.Explicit)]
struct SA {
    [FieldOffset(0)] int f0;
    [FieldOffset(4)] SB sb;
    [FieldOffset(4)] object f4;
    [FieldOffset(8)] int f8;
    [FieldOffset(12)] int f12;
    [FieldOffset(16)] int f16;
}
struct SB { int i; int j; object o1; int k; }

// Two `object` fields in `SB`
// Gives the following layout:

// | f0 | f4 and o1 | f8 and o2 | f12 and i | f16 and j | k |

// If I add another `object` field after the first one in `SB`, i *have* to convert
// `f8` to `object`, otherwise I get a `TypeLoadException`.
// No other field will do.

[StructLayout(LayoutKind.Explicit)]
struct SA {
    [FieldOffset(0)] int f0;
    [FieldOffset(4)] SB sb;
    [FieldOffset(4)] object f4;
    [FieldOffset(8)] object f8;
    [FieldOffset(12)] int f12;
    [FieldOffset(16)] int f16;
}
struct SB { int i; int j; object o1; object o2; int k; }

person Suzanne Soy    schedule 13.03.2013    source источник


Ответы (2)


Является ли это лазейкой в ​​предупреждениях/ошибках компилятора?

Нет, в этом нет ничего плохого. Поля могут перекрываться, поэтому LayoutKind.Explicit существует в первую очередь. Это позволяет объявить эквивалент union в неуправляемом коде, который иначе не поддерживается в C#. Вы не можете внезапно прекратить использование [FieldOffset] в объявлении структуры, среда выполнения настаивает на том, чтобы вы использовали его для всех членов структуры. Не является технически необходимым, но является простым требованием, позволяющим избежать неправильных предположений.

кажется, что все ссылочные (объектные) поля в SB перемещены

Да, это нормально. CLR размещает объекты недокументированным и недоступным для обнаружения способом. Точные правила, которые он использует, не задокументированы и могут быть изменены. Это также не будет повторяться для разных джиттеров. Макет не становится предсказуемым до тех пор, пока объект не будет маршалирован, вызовом Marshal.StructureToPtr() или неявным образом упаковщиком pinvoke. Это единственный раз, когда точная компоновка имеет значение. Я написал о причинах такого поведения в этом ответе.

person Hans Passant    schedule 13.03.2013
comment
Если я не ошибаюсь, структура также имеет упорядоченную компоновку, когда мы приводим необработанный небезопасный указатель к указателю на эту структуру, не так ли? Скажем, в unsafe { SA* sa = (SA*)0x1234; /* Use sa here */ } sa будет иметь тот же макет, что и при сортировке, не так ли? - person Suzanne Soy; 13.03.2013
comment
Нет, это не маршалинг. Компилятор не позволит вам это сделать, структура не преобразовывается. Попытайся. - person Hans Passant; 13.03.2013
comment
Действительно, он работает со структурами, содержащими только целые числа, но не когда я добавляю поле объекта. Однако вопрос по-прежнему актуален для структур, которые содержат только int и тому подобное, например, первый, который я использовал. - person Suzanne Soy; 13.03.2013

Ответ на первый вопрос — нет, в сообщениях об ошибках компилятора нет лазейки или ошибки. Если вы начнете делать явную компоновку, компилятор будет считать, что вы знаете, что делаете (в определенных пределах — см. ниже). Вы сказали ему накладывать одну структуру поверх другой. Компилятор не заботится (и не должен) о том, что структура, которую вы накладываете, также не выложена явно.

Если бы компилятору было это важно, вы бы не смогли наложить любой тип, который не был явно указан, а это означает, что вы не могли бы выполнить объединение в общем случае. Рассмотрим, например, попытку наложения DateTime и long:

[StructLayout(LayoutKind.Explicit)]
struct MyUnion
{
    [FieldOffset(0)]
    public bool IsDate;
    [FieldOffset(1)]
    public DateTime dt;
    [FieldOffset(1)]
    public long counter;
}

Это не скомпилировалось бы, если бы DateTime не было указано явно. Вероятно, это не то, что вы хотите.

Что касается размещения ссылочных типов в явно выложенных структурах, ваши результаты будут... вероятно, не такими, как вы ожидали. Рассмотрим, например, этот простой бит:

struct MyUnion
{
    [FieldOffset(0)]
    public object o1;
    [FieldOffset(0)]
    public SomeRefType o2;
}

Это сильно нарушает безопасность типов. Если он скомпилируется (что вполне возможно), он умрет с TypeLoadException, когда вы попытаетесь его использовать.

Компилятор не позволит вам нарушить безопасность типов, где это возможно. Я не знаю, знает ли компилятор, как обрабатывать эти атрибуты и размещать структуру, или он просто передает информацию о макете среде выполнения через сгенерированный MSIL. Вероятно, последнее, учитывая ваш второй пример, где компилятор разрешил определенный макет, но среда выполнения взорвалась с TypeLoadException.

Поиск в Google по [structlayout.explicit reference types] выявил несколько интересных дискуссий. См., например, Наложение нескольких полей ссылок CLR друг на друга в явной структуре?.

person Jim Mischel    schedule 13.03.2013
comment
Для структуры MyUnion нет исключения TypeLoadException. Это произойдет только тогда, когда вы перекроете значение типа значения объектом ссылочного типа. Как это сделал ОП. Попытайся. - person Hans Passant; 13.03.2013
comment
Это кажется... опасным. Или, возможно, нет. Хммм. . . Я думаю, было бы безопасно наложить два ссылочных типа. Исключение времени выполнения произойдет, если я попытаюсь разыменовать o2 после установки o1 на что-то, что не является SomeRefType. - person Jim Mischel; 13.03.2013
comment
Мне это кажется опасным: вы можете наложить два массива с разными типами. Это позволяет вам получить доступ к памяти за пределами меньшей области массива. - person Ark-kun; 24.11.2014
comment
@ Ark-kun: среда выполнения предотвратит наложение ссылочного типа на тип значения. Если вы накладываете два типа значений, объем выделенной памяти будет больше из двух. Так что сценарий, на который вы намекаете, не является проблемой. - person Jim Mischel; 24.11.2014
comment
@JimMischel Наложение двух массивов не означает наложение ссылочного типа на тип значения. Массивы — это ссылки в .Net. Наложение массивов позволяет обойти проверку типов CLR и защиту границ массива, а также повредить память. - person Ark-kun; 25.11.2014
comment
@Ark-kun: массивы в .NET являются ссылочными типами. Итак, опять же, нет опасности обойти безопасность типов. - person Jim Mischel; 25.11.2014
comment
@JimMischel Все дело в названии. У нас есть указатель на ячейку размером 1024 байта. Этот указатель можно рассматривать как как byte[1024], так и как int[1024]. В последнем случае только первые 256 элементов массива указывают на выделенный 1024-байтовый блок памяти. Запись в старшие элементы соответствует доступу к памяти за пределами выделенной области. Вы перезаписываете память, которая не принадлежит вашим переменным. Неважно, как вы это называете, это обход безопасности безопасной памяти, что даже хуже, чем обход безопасности типов. Почему бы не попробовать? - person Ark-kun; 25.11.2014
comment
@Ark-kun: Массивы являются ссылочными типами. Ссылка на массив в этой структуре будет занимать sizeof(IntPtr) байт. Сам массив хранится в отдельном блоке памяти. Нет шансов нарушить безопасность типов. Теперь, если вы говорите о буферах фиксированного размера, у вас есть законная забота. Но это разрешено только в небезопасном контексте, в отношении которого уже существуют проблемы, о которых вы говорите. - person Jim Mischel; 25.11.2014
comment
@JimMischel Массивы являются ссылочными типами. - Да. Ссылка на массив в этой структуре будет занимать sizeof(IntPtr) байт. - Да. Сам массив хранится в отдельном блоке памяти. - Да. Элементы хранятся в блоке кучи. Наряду со всеми другими объектами кучи, принадлежащими вашему процессу. Теперь, если вы говорите о буферах фиксированного размера - нет, просто обычные управляемые массивы в куче. Нет шансов нарушить безопасность типов. - Произвольная перезапись памяти, которая не принадлежит вашим объектам !в безопасном контексте! нарушает все, включая безопасность типов. Вы так не думаете? - person Ark-kun; 26.11.2014
comment
@JimMischel Предположим, вы могли бы успешно выполнить byte[] a = new byte[10]; a[2000] = 11; Если бы это сработало, это было бы катастрофой, верно? Но это именно то, чего я добился. - person Ark-kun; 26.11.2014