В CLR есть особенность, что при загрузки сборки через Assembly.Load
или через Assembly.ReflectionOnlyLoad
, загружаются все сборки по мере запроса. В отличии от констант и их типов, они заранее копируются в дочернюю сборку и больше не зависят от родительской сборки. Но в определённых случаях типы констант не копируются в дочернюю сборку и их изменение может сломать работу дочерней сборки, несмотря на то, что тип константы не должен этого делать. Эта статья Вам поможет разобраться в каких случаях это может произойти.
При компиляции любых сборок в .NET все значения констант копируются в дочернюю сборку и при попытке, без компиляции, подменить родительскую сборку вручную, приведёт к тому что константы в дочерней сборке - не изменятся и CLR будет использовать значения, которые были перенесены в дочернюю сборку.
Однако, изменение типа константы в родительской сборки и подмена её без компиляции в дочернюю сборку - может привести к CLR ошибке при определённых условиях:
Unhandled Exception: System.Reflection.CustomAttributeFormatException: Binary format of the specified custom attribute was invalid. at System.Reflection.CustomAttribute.GetCustomAttributes(RuntimeModule decoratedModule, Int32 decoratedMetadataToken, Int32 pcaCount, RuntimeType attributeFilterType, Boolean mustBeInheritable, IList derivedAttributes, Boolean isDecoratedTargetSecurityTransparent) at System.Reflection.CustomAttribute.GetCustomAttributes(RuntimeMethodInfo method, RuntimeType caType, Boolean inherit) at EnumTestApp.Program.Main(String[] args)
Следует разобраться, что происходит в CLI и почему смена типа константы может привести к таким последствиям:
Для эксперимента использовано решение из 2х проектов, с применением .NET Framework (но можно собрать аналогичное приложение на последних версиях .NET).
Родительская (Common) сборка состоит из одного файла с enum размерностью в int
:
/* @echo off && cls set WinDirNet=%WinDir%\Microsoft.NET\Framework IF EXIST "%WinDirNet%\v2.0.50727\csc.exe" set csc="%WinDirNet%\v2.0.50727\csc.exe" IF EXIST "%WinDirNet%\v3.5\csc.exe" set csc="%WinDirNet%\v3.5\csc.exe" IF EXIST "%WinDirNet%\v4.0.30319\csc.exe" set csc="%WinDirNet%\v4.0.30319\csc.exe" %csc% /nologo /target:library /out:"CommonLib.dll" %0 goto :eof */ using System; namespace CommonLib { public enum SharedEnum : int { Undefined = 0, First = 1, Second = 2, Third = 3, } }
Дочернее консольное приложение будет содержать в себе атрибут и тестовый код:
/* @echo off && cls IF EXIST "%~0.exe" ( "%~0.exe" exit ) call CommonLib.bat set WinDirNet=%WinDir%\Microsoft.NET\Framework IF EXIST "%WinDirNet%\v2.0.50727\csc.exe" set csc="%WinDirNet%\v2.0.50727\csc.exe" IF EXIST "%WinDirNet%\v3.5\csc.exe" set csc="%WinDirNet%\v3.5\csc.exe" IF EXIST "%WinDirNet%\v4.0.30319\csc.exe" set csc="%WinDirNet%\v4.0.30319\csc.exe" %csc% /nologo /reference:CommonLib.dll /out:"%~0.exe" %0 "%~0.exe" PAUSE exit */ using System; using System.Reflection; using CommonLib; namespace EnumTestApp { internal class Program { static void Main(string[] args) { EnumAttribute attr = (EnumAttribute)typeof(Program) .GetMethod("MethodWithEnumAttribute", BindingFlags.Static | BindingFlags.NonPublic) .GetCustomAttributes(typeof(EnumAttribute), false)[0]; Console.WriteLine(attr.Value.ToString()); } [Enum(Value = SharedEnum.Third)] static void MethodWithEnumAttribute() { } } internal class EnumAttribute : Attribute { public SharedEnum Value { get; set; } } }
Консольное приложение при запуске читает значение атрибута метода MethodWithEnumAttribute
и выводит его в консоль.
При выполнении файла ConsoleApp.bat
, то в командной строке мы увидим строку: Third
. Теперь удалим значение Third
из файла CommonLib.bat
. Следует произвести запуск файла CommonLib.bat
для обновления сборки и повторный запуск файла ConsoleApp.bat
. Как и следовало ожидать, значение сохраняется в консольном приложении, поэтому вместо Third
выводится цифра 3. Из чего следует, что константа осталась неизменной в сборке ConsoleApp.bat
, несмотря на то, что в реальности её больше нет в CommonLib
.
Далее следует изменить тип emum
с int
на byte
в CommonLib.bat
:
public enum SharedEnum : byte //int
После чего сделаем перекомпиляцию CommonLib.bat
и снова запустим консольное приложение: ConsoleApp.bat
В результате и происходит вышеописанная ошибка:
System.Reflection.CustomAttributeFormatException: Binary format of the specified custom attribute was invalid
Чтобы понять что случилось, потребуется углубиться в дебри стандарта ECMA-335 на котором и основан CLI. Метаданные хранятся в 44 таблицах в числе которых есть таблица CustomAttribute
(Для визуализации я использую приложение PE Image Info Plugin):
В поле Value представляет из себя сигнатуру (Аргументы и значения экземпляра атрибута) описанную в разделе II.23.3 стандарта ECMA-335
0x0001 — Первые 2 байта представляют из себя константу Prolog
.
Затем следует массив аргументов для создания экземпляра объекта. Кол-во аргументов описано в таблицах MethodDef
или MemberRef
. (В нашем случае это MethodDef
, ибо атрибут объявлен внутри нашей сборки, у нашего атрибута нет конструктора с аргументами и, поэтому, в массиве его нет и размерность его равняется нулю).
0x0001 — Следующие 2 байта информируют о количестве именованных свойств и их значений. В нашем случае это будет одно свойство Value со значением SharedEnum.Third
.
0x54 — Значение идентифицирует тип объекта — Property
(Тут может быть только 0x54 или 0x53 - Field
).
0x55 — Это тип значения - Enum
.
0x56 — Представляет из себя упакованное int (II.23.2) значение длины типа нашего свойства.
0x43…0x6C — Строковое представление типа нашего свойства: “CommonLib.SharedEnum, CommonLib, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null”
0x05 — Представляет из себя упакованное int значение длинны названия нашего свойства.
0x56-0x65 — Представляет из себя название свойства Value
.
И последние 4 байта представляют из себя само значение enum
, которое мы передали в качестве значения в свойство Value
атрибута: SharedEnum.Third = 3
.
В данной сигнатуре можно заметить, что размер всего свойства никак не определён. Обычно для размера объекта используется одна из следующих техник:
Count
, каждый Prop
или Field
начинается с размера всего поля. Для примера, как это сделано со строками в примере, где каждая строка начинается с Length
.В отсутствии разделителя и заключается проблема (получается исключение описанное выше):
У нас нет возможности понять сколько занимает значение Enum
, а без этого мы не можем прочитать значение: Int64
, Int32
, Int16
или Byte
?. То есть, для чтения всей этой сигнатуры, CLR загружает сборку CommonLib.dll
, находит в метаданных описание этого enum
, получает его размерность и только после этого он имеет возможность корректно прочитать всю сигнатуру целиком.
Данная проблема касается именно значений типа enum
, так как для всех остальных данных можно понять тип или прочитать их размерность.
В качестве практической части,следует задать вопрос почему же не указана размерность значения и какие варианты являются ошибочными и почему сделано именно так:
Это маловероятно, так как мы имеем полный путь к типу, а не ссылку на таблицу AssemblyRef
(В этой таблице хранятся ссылки на все зависимые сборки). И так-же хранится название свойства, а не ссылка вида TypeDefOrRef
coded index (II.24.2.6). При этом, длина константных строк хранится в упакованном виде, т.е. может занимать от одного до 4х байт.
Логика маппинга заключается в том, что массив байт должен быть фиксированной длины, чтобы его можно было быстро положить в память, накрыв его “трафаретом” → структурой.
Положительный ответ на этот вопрос является маловероятным, т.к. во первых, как описано выше, — строки дублируются вместо ссылок в другие таблицы, во вторых значение Enum (0x55)
можно было заменить на значение базового типа,в качестве примера можно рассмотреть CorElementType.ELEMENT_TYPE_I4 потому что, мы видели на примере выше, удаление значение Enum
— не ломает приложение и вместо определённого значения Third
было получено просто 3.
В этом случае существует проблема — нет возможности через рефлексию понять, что это значение enum
из внешней сборки, и поэтому теряется оригинальное приведение типов.
А если заменить тип свойства Value
в атрибуте с SharedEnum
на Object
, то сигнатура будет включать в себя не только тип переменной Enum (0x55)
, но ещё и Boxed (0x51)
.
internal class EnumAttribute : Attribute { public Object Value { get; set; } }
Вот так-бы выглядела сигнатура нашего атрибута, если в свойство Value
передать значение 3 (Int32 - CorElementType.ELEMENT_TYPE_I4 - 0x08):
[Enum(Value = 3)]
Моё мнение, архитекторы бинарного формата пожалев один байт для хранения типа enum
— усложнили работу не только загрузчику CLR, но и добавив неопределённости при работе с константами.
А как Вы считаете, какой был вложен смысл в том, чтобы не добавлять размерность значения enum
в CLI?