Почему LayoutKind.Sequential работает иначе, если структура содержит поле DateTime? - PullRequest
20 голосов
/ 09 ноября 2010

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

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

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

Now "First" имеет тот же адрес, что и структура.

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

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

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

[EDIT3] Первоначальный источник этого вопроса был с форумов MSDN C #:

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

Ответы [ 6 ]

15 голосов
/ 23 февраля 2014

Почему 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 структурой макета.

6 голосов
/ 13 мая 2014

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

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

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

Там!Разве это не очевидно?

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

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

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

3 голосов
/ 09 ноября 2010

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

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

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

Вопрос «Может ли кто-нибудь дать объяснение?».

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

2 голосов
/ 09 ноября 2011

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

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

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

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

2 голосов
/ 09 ноября 2010

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

  • удваиваются намного быстрее, если они выровнены
  • Кэши ЦП могут работать лучше, если в пораженном элементе нет «дыр»

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

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

1 голос
/ 15 февраля 2011

Если вы собираетесь взаимодействовать с C / 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 никак не может быть маршалированным, только в строку (bingle Marshal DateTime).

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

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

...