Почему LayoutKind.Sequential работает по-разному, если структура содержит поле DateTime?

Почему LayoutKind.Sequential работает по-разному, если структура содержит поле DateTime?

Рассмотрим следующий код (консольное приложение, которое должно быть скомпилировано с "небезопасным" ):

using System;
using System.Runtime.InteropServices;

namespace ConsoleApplication3
{
    static class Program
    {
        static void Main()
        {
            Inner test = new Inner();

            unsafe
            {
                Console.WriteLine("Address of struct   = " + ((int)&test).ToString("X"));
                Console.WriteLine("Address of First    = " + ((int)&test.First).ToString("X"));
                Console.WriteLine("Address of NotFirst = " + ((int)&test.NotFirst).ToString("X"));
            }
        }
    }

    [StructLayout(LayoutKind.Sequential)]
    public struct Inner
    {
        public byte First;
        public double NotFirst;
        public DateTime WTF;
    }
}

Теперь, если я запустил код выше, я получаю вывод, похожий на следующий:

Адрес struct = 40F2CC
Адрес First = 40F2D4
Адрес NotFirst = 40F2CC

Обратите внимание, что адрес First не совпадает с адресом структуры; однако адрес NotFirst совпадает с адресом структуры.

Теперь закомментируйте поле "DateTime WTF" в структуре и запустите его снова. На этот раз я получаю аналогичный результат:

Адрес struct = 15F2E0
Адрес First = 15F2E0
Адрес NotFirst = 15F2E8

Теперь "Первый" имеет тот же адрес, что и структура.

Я нахожу это поведение неожиданным, учитывая использование LayoutKind.Sequential. Может ли кто-нибудь дать объяснение? Имеет ли такое поведение какие-либо последствия при взаимодействии с структурами C/С++, использующими тип Com DATETIME?

[EDIT] ПРИМЕЧАНИЕ. Я подтвердил, что, когда вы используете Marshal.StructureToPtr() для маршалирования структуры, данные сортируются в правильном порядке с первым полем "Первое". Кажется, это говорит о том, что он будет работать нормально с interop. Тайна заключается в том, почему меняется внутренняя компоновка, но, конечно, внутренняя компоновка никогда не указана, поэтому компилятор может делать то, что ей нравится.

[EDIT2] Удалено "небезопасно" из объявления структуры (это осталось от некоторого тестирования, которое я делал).

[EDIT3] Исходный источник этого вопроса был из форумов MSDN С#:

http://social.msdn.microsoft.com/Forums/en-US/csharplanguage/thread/fb84bf1d-d9b3-4e91-823e-988257504b30

Ответ 1

Почему LayoutKind.Sequential работает по-разному, если структура содержит поле DateTime?

Это связано с (удивительным) фактом, что DateTime сам имеет макет "Авто" (ссылка на вопрос SO самостоятельно). Этот код воспроизводит поведение, которое вы видели:

static class Program
{
    static unsafe void Main()
    {
        Console.WriteLine("64-bit: {0}", Environment.Is64BitProcess);
        Console.WriteLine("Layout of OneField: {0}", typeof(OneField).StructLayoutAttribute.Value);
        Console.WriteLine("Layout of Composite: {0}", typeof(Composite).StructLayoutAttribute.Value);
        Console.WriteLine("Size of Composite: {0}", sizeof(Composite));
        var local = default(Composite);
        Console.WriteLine("L: {0:X}", (long)(&(local.L)));
        Console.WriteLine("M: {0:X}", (long)(&(local.M)));
        Console.WriteLine("N: {0:X}", (long)(&(local.N)));
    }
}

[StructLayout(LayoutKind.Auto)]  // also try removing this attribute
struct OneField
{
    public long X;
}

struct Composite   // has layout Sequential
{
    public byte L;
    public double M;
    public OneField N;
}

Пример вывода:

64-bit: True
Layout of OneField: Auto
Layout of Composite: Sequential
Size of Composite: 24
L: 48F050
M: 48F048
N: 48F058

Если мы удалим атрибут из OneField, все будет вести себя так, как ожидалось. Пример:

64-bit: True
Layout of OneField: Sequential
Layout of Composite: Sequential
Size of Composite: 24
L: 48F048
M: 48F050
N: 48F058

Этот пример представлен с помощью компиляции платформы x64 (поэтому размер 24, три раза восемь) неудивительно), но также с x86 мы видим те же "неупорядоченные" адреса указателей.

Поэтому я могу заключить, что макет OneField (соответственно DateTime в вашем примере) влияет на макет структуры, содержащей член OneField, даже если сама составная структура имеет макет Sequential. Я не уверен, что это проблематично (или даже требуется).


Согласно комментарию Ханса Пассанта в другом потоке, он больше не пытается сохранить его последовательным, когда один из членов является структурой Auto layout.

Ответ 2

Перейдите к описанию правил компоновки более тщательно. Правила компоновки управляют только макетом, когда объект экспонируется в неуправляемой памяти. Это означает, что компилятор может свободно размещать поля, но он хочет, пока объект фактически не экспортируется. Скорее всего, это даже верно для FixedLayout!

Ian Ringrose прав насчет проблем эффективности компилятора, и это учитывает окончательный макет, который здесь выбирается, но он не имеет ничего общего с тем, почему компилятор игнорирует вашу спецификацию макета.

Несколько человек указали, что DateTime имеет автоматическую компоновку. Это конечный источник вашего удивления, но причина немного неясна. Документация для Auto layout говорит, что "объекты, определенные с макетом [Auto], не могут быть выставлены за пределами управляемого кода. Попытка сделать это создает исключение". Также обратите внимание, что DateTime - это тип значения. Включив в свою структуру тип значения, имеющий автоматическую компоновку, вы непреднамеренно пообещали, что никогда не будете подвергать содержащую структуру неуправляемому коду (потому что это приведет к экспонированию DateTime, и это приведет к возникновению исключения). Поскольку правила компоновки управляют только объектами в неуправляемой памяти, и ваш объект никогда не может быть подвергнут неуправляемой памяти, компилятор не ограничен в выборе макета и свободен делать все, что захочет. В этом случае он возвращается к политике автоматического макета, чтобы добиться лучшей упаковки и выравнивания структуры.

Там! Это было не так очевидно!

Все это, кстати, распознается при статическом времени компиляции. Фактически, компилятор распознает это, чтобы решить, что он может игнорировать вашу директиву макета. Узнав это, предупреждение от компилятора, похоже, будет в порядке. Вы на самом деле ничего не сделали неправильно, но полезно сказать, когда вы написали что-то, что не имеет никакого эффекта.

Различные комментарии здесь, рекомендующие исправленный макет, как правило, являются хорошим советом, но в этом случае это не обязательно будет иметь никакого эффекта, потому что включение поля DateTime освобождает компилятор от почитания макета вообще. Хуже того: компилятор не обязан соблюдать макет, но он свободен в плане макета. Это означает, что последовательные версии CLR могут свободно вести себя по этому поводу.

Обработка макета, на мой взгляд, является ошибкой дизайна в CLI. Когда пользователь указывает макет, компилятор не должен заниматься адвокацией вокруг них. Лучше держать вещи простыми и заставить компилятор делать то, что сказано. Тем более, что касается макета. "Умный", как мы все знаем, представляет собой четырехбуквенное слово.

Ответ 3

Чтобы ответить на мои собственные вопросы (как советовали):

Вопрос: "Имеет ли такое поведение какие-либо последствия при взаимодействии с структурами C/С++, которые используют тип Com DATETIME?"

Ответ: Нет, потому что макет соблюдается при использовании Marshalling. (Я проверил это эмпирически.)

Вопрос "Кто-нибудь может дать объяснение?".

Ответ: Я все еще не уверен в этом, но поскольку внутреннее представление структуры не определено, компилятор может делать то, что ему нравится.

Ответ 4

Несколько факторов

  • удваивается намного быстрее, если они выровнены
  • Кэш-память CPU может работать лучше, если нет "дыр" в ударе

Итак, у компилятора С# есть несколько недокументированных правил, которые он использует, чтобы попытаться получить "лучший" макет структур, эти правила могут принимать во внимание общий размер структуры и/или если она содержит другую структуру и т.д. Если вам нужно знать макет структуры, вы должны указать его самостоятельно, а не позволить компилятору решить.

Однако LayoutKind.Sequential останавливает компилятор, изменяя порядок полей.

Ответ 5

Вы проверяете адреса, как они находятся в управляемой структуре. Атрибуты маршала не гарантируют расположение полей внутри управляемых структур.

Причина, по которой он правильно упорядочивается в нативных структурах, заключается в том, что данные копируются в исходную память, используя атрибуты, установленные значениями маршала.

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

Если настройка полей с атрибутами маршала хранилась в управляемых данных так же, как и исходные данные, тогда в Marshal.StructureToPtr не было бы никакого смысла, вы просто байт-скопировали данные.

Ответ 6

Если вы собираетесь взаимодействовать с C/С++, я всегда буду определен с StructLayout. Вместо Sequential я бы пошел с Explicit и задал каждую позицию с помощью FieldOffset. Кроме того, добавьте переменную Pack.

[StructLayout(LayoutKind.Explicit, Pack=1, CharSet=CharSet.Unicode)]
public struct Inner
{
    [FieldOffset(0)]
    public byte First;
    [FieldOffset(1)]
    public double NotFirst;
    [FieldOffset(9)]
    public DateTime WTF;
}

Похоже, что DateTime не может быть Marshaled в любом случае, только для строки (bingle Marshal DateTime).

Переменная Pack особенно важна в коде С++, который может быть скомпилирован в разных системах с разными размерами слов.

Я бы также проигнорировал адреса, которые можно увидеть при использовании небезопасного кода. На самом деле не имеет значения, что делает компилятор, пока Marshaling корректен.