Почему LayoutKind.Sequential работает по-другому, если структура содержит поле DateTime?
Почему 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;
}
}
Теперь, если я запускаю приведенный выше код, я получаю вывод, подобный следующему:
Адрес структуры = 40F2CC
Адрес первого = 40F2D4
Адрес NotFirst = 40F2CC
Обратите внимание, что адрес First НЕ совпадает с адресом структуры; однако адрес NotFirst совпадает с адресом структуры.
Теперь закомментируйте поле DateTime WTF в структуре и запустите его снова. На этот раз я получаю вывод, похожий на этот:
Адрес struct = 15F2E0
Адрес первого = 15F2E0
Адрес NotFirst = 15F2E8
Теперь "Первый" имеет тот же адрес, что и структура.
Я нахожу это поведение удивительным, учитывая использование LayoutKind.Sequential. Кто-нибудь может дать объяснение? Имеет ли это поведение какие-либо последствия при взаимодействии со структурами C/C++, использующими тип Com DATETIME?
[РЕДАКТИРОВАТЬ] ПРИМЕЧАНИЕ. Я убедился, что когда вы используете Marshal.StructureToPtr() для маршалинга структуры, данные маршалируются в правильном порядке, при этом поле "First" будет первым. Похоже, это говорит о том, что он будет хорошо работать с взаимодействием. Загадка заключается в том, почему меняется внутреннее расположение, но, разумеется, внутреннее расположение никогда не указывается, поэтому компилятор может делать то, что ему нравится.
[EDIT2] Удалил "unsafe" из объявления структуры (это было после некоторого тестирования, которое я проводил).
[EDIT3] Первоначальный источник этого вопроса был с форумов MSDN C#:
6 ответов
Почему LayoutKind.Sequential работает по-другому, если структура содержит поле DateTime?
Это связано с (удивительным) фактом, что DateTime
Сам по себе имеет макет "Авто" (ссылка на ТА вопрос сам). Этот код воспроизводит поведение, которое вы видели:
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-разрядный: правда Расположение OneField: Авто Компоновка Композит: Последовательный Размер Композита: 24 L: 48F050 М: 48F048 N: 48F058
Если мы удалим атрибут из OneField
, вещи ведут себя как ожидалось. Пример:
64-разрядный: правда Макет OneField: последовательный Компоновка Композит: Последовательный Размер Композита: 24 L: 48F048 М: 48F050 N: 48F058
Это пример компиляции платформы x64 (поэтому неудивительно, что размер 24, трижды восемь), но и с x86 мы видим те же "неупорядоченные" адреса указателей.
Так что я думаю, что могу сделать вывод, что макет OneField
(Соотв. DateTime
в вашем примере) имеет влияние на макет структуры, содержащей OneField
член, даже если сама эта составная структура имеет макет Sequential
, Я не уверен, если это проблематично (или даже требуется).
Согласно комментарию Ганса Пассанта в другой ветке, он больше не делает попытки сохранить его последовательным, когда один из участников является Auto
структура макета.
Пойдите, прочитайте спецификацию для правил расположения более тщательно. Правила макета управляют макетом только тогда, когда объект выставлен в неуправляемой памяти. Это означает, что компилятор может свободно размещать поля по своему усмотрению, пока объект не будет экспортирован. К моему удивлению, это даже верно для FixedLayout!
Йен Рингроз прав в вопросах эффективности компилятора, и это учитывает окончательный макет, который выбирается здесь, но это не имеет никакого отношения к тому, почему компилятор игнорирует вашу спецификацию компоновки.
Несколько человек отметили, что у DateTime есть Auto layout. Это основной источник вашего удивления, но причина немного неясна. В документации для автоматической разметки сказано, что "объекты, определенные с разметкой [Авто], не могут быть представлены вне управляемого кода. Попытка сделать это приводит к исключению". Также обратите внимание, что DateTime является типом значения. Включив тип значения с автоматическим макетом в вашу структуру, вы непреднамеренно пообещали, что никогда не будете подвергать содержащую структуру неуправляемому коду (потому что при этом будет выставлен DateTime, и это сгенерирует исключение). Поскольку правила компоновки управляют только объектами в неуправляемой памяти, и ваш объект никогда не может подвергаться неуправляемой памяти, компилятор не ограничен в выборе компоновки и может делать все, что захочет. В этом случае он возвращается к политике автоматического размещения, чтобы добиться лучшей упаковки и выравнивания структуры.
Там! Разве это не очевидно?
Кстати, все это можно узнать во время статической компиляции. Фактически, компилятор распознает его, чтобы решить, что он может игнорировать вашу директиву макета. Признав это, здесь должно показаться предупреждение от компилятора. Вы на самом деле не сделали ничего плохого, но полезно знать, когда вы пишете что-то, что не имеет никакого эффекта.
Различные комментарии, рекомендующие фиксированный макет, как правило, являются хорошим советом, но в этом случае это не обязательно будет иметь какой-либо эффект, потому что включение поля DateTime освобождает компилятор от компоновки. Хуже того: компилятор не обязан соблюдать компоновку, но он свободен для компоновки. Это означает, что последующие версии CLR могут свободно вести себя по-другому.
На мой взгляд, обработка макета является недостатком дизайна в CLI. Когда пользователь указывает макет, компилятор не должен заниматься им. Лучше быть проще и заставить компилятор делать то, что ему говорят. Особенно в том, что касается макета."Умный", как мы все знаем, это слово из четырех букв.
Чтобы ответить на мои собственные вопросы (как советовали):
Вопрос: "Имеет ли это поведение какие-либо последствия при взаимодействии со структурами C/C++, использующими тип Com DATETIME?"
Ответ: Нет, поскольку компоновка соблюдается при использовании Marshalling. (Я подтвердил это опытным путем.)
Вопрос "Кто-нибудь может дать объяснение?".
Ответ: Я до сих пор не уверен в этом, но поскольку внутреннее представление структуры не определено, компилятор может делать то, что ему нравится.
Несколько факторов
- двойники намного быстрее, если они выровнены
- Кеши ЦП могут работать лучше, если в пораженном месте нет "дыр"
Таким образом, компилятор C# имеет несколько недокументированных правил, которые он использует, чтобы попытаться получить "наилучшую" компоновку структур, эти правила могут учитывать общий размер структуры и / или если она содержит другую структуру и т. Д. Если вам нужно Если вы знаете макет структуры, вы должны указать ее самостоятельно, а не позволять компилятору решать.
Однако LayoutKind.Sequential останавливает компилятор, изменяющий порядок полей.
Вы проверяете адреса, как они находятся в управляемой структуре. Атрибуты маршала не имеют гарантий для расположения полей в управляемых структурах.
Причина, по которой он правильно маршалирует в собственные структуры, заключается в том, что данные копируются в собственную память с использованием атрибутов, установленных маршальными значениями.
Таким образом, расположение управляемой структуры не влияет на расположение нативной структуры. Только атрибуты влияют на расположение нативной структуры.
Если поля, настроенные с помощью атрибутов маршала, хранятся в управляемых данных так же, как и в собственных данных, тогда в Marshal.StructureToPtr не будет никакого смысла, вам просто нужно скопировать данные с помощью байтов.
Если вы собираетесь взаимодействовать с 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 не может быть Marshaled в любом случае, только в строку (bingle Marshal DateTime).
Переменная Pack особенно важна в коде C++, который может быть скомпилирован в разных системах с разными размерами слов.
Я бы также проигнорировал адреса, которые можно увидеть при использовании небезопасного кода. Неважно, что делает компилятор, пока маршалинг правильный.