Формат файла .NET – внутренняя структура сигнатур, часть 2 из 2 - Элементы
ОГЛАВЛЕНИЕ
2. Элементы
Были рассмотрены все сигнатуры, но это не конец. Сигнатуры состоят из более мелких частей, именуемых "элементы", они были отделены, так как находятся в составе более чем одной сигнатуры, а значит, не надо повторять объяснение для конкретного элемента(ов) в каждой сигнатуре. В данной главе они разобраны более детально.
2.1 CustomMod
Этот элемент часто повторялся в разобранных сигнатурах, и поэтому он идет первым. Пользовательские модификаторы похожи на пользовательские атрибуты, но, в отличие от них, пользовательские модификаторы входят в состав сигнатуры. Пользовательские модификаторы определяются в CIL с помощью ключевых слов modreq (обязательный модификатор) и modopt (необязательный модификатор) в объявлении метода, оба требуют задания типа (класс или структура) в качестве их "аргумента". Две сигнатуры, различающиеся только по добавлению пользовательского модификатора (обязательного или необязательного), не должны считаться совпадающими, и, как говорит спецификация:
Различие между обязательными и необязательными модификаторами важно для инструментов, отличных от CLI, имеющих дело с метаданными, как правило, компиляторов и анализаторов программ. Обязательный модификатор показывает, что в измененном элементе есть специальная семантика, которую нельзя игнорировать, тогда как необязательный модификатор можно игнорировать. Например, префикс const(константа) в языке программирования C может быть оформлен необязательным модификатором, так как вызывающий оператор метода, имеющий параметр с префиксом const, не должен обрабатывать его особым образом. Наоборот, параметр, который должен быть создан с помощью копирующего конструктора в C++, должен быть помечен обязательным пользовательским атрибутом, так как он является вызывающим оператором, делающим копию.
К сожалению, C# имеет проблемы с обработкой параметров с прикрепленными пользовательскими модификаторами, читайте об этом в статьях Modopt, сигнатуры методов, и неполные спецификации и Подробнее о modopt на CodeBetter.com.
CMOD_OPT и CMOD_REQD – константы, определенные в таблице констант в первой части, элементы TypeDefEncoded и TypeRefEncoded являются одним элементом TypeDefOrRefEncoded, тщательно разобранным в следующем разделе. Заметьте, что к полю, свойству, параметру или возвращаемому параметру может быть прикреплено ноль, один или больше CustomMod. Нельзя определить пользовательский модификатор с помощью C#, исключая System.Reflection.Emit. В пространстве имен System.Runtime.CompilerServices есть несколько индикаторов, которые можно применить к пользовательскому модификатору, например, CallConvCdecl, IsConst, IsLong.
Рисунок 3. Схема синтаксиса элемента CustomMod
Пример 1
В примере ниже поле TestField помечено модификатором modreq, так как CustomMod находится внутри сигнатуры FieldSig, показанной в самом начале статьи на рисунке 2. Индикатор IsLong отличает long от целого в C++, но в данном случае в основе этого пользовательского модификатора нет специальной семантики, лишь показан формат элемента CustomMod в сигнатуре. Значение элемента TypeDefOrRefEncoded показано дважды, в двух системах счисления - шестнадцатеричной (подпись внизу 16) и двоичной (подпись внизу 2). В следующем подразделе узнаете, почему.
// Полный исходник: CustomMod\1.il
// Двоичный файл: CustomMod\1.dll
// (...)
.field public int64 modreq([mscorlib]System.CompilerServices.IsLong) TestField
Таблица ниже показывает всю сигнатуру FieldSig, индексируемую столбцом Field.Signature, вместе с встроенным пользовательским модификатором, сгенерированным ключевым словом modreq.
Смещение |
Значение |
Что означает |
|
|
размер сигнатуры |
|
|
|
|
|
встреченный пользовательский обязательный модификатор ( |
|
|
Элемент |
|
|
тип поля ( |
2.2 TypeDefOrRefEncoded
Теперь рассмотрим самый загадочный элемент TypeDefOrRefEncoded, он не так сложен, как кажется. Этот элемент определяет, в какой таблице метаданных и в какой строке таблицы хранится информация о ссылочном типе. Первые два самые младшие биты кодируют таблицу метаданных: 0 - для TypeDef (ссылочный тип хранится в текущей сборке), 1 – для TypeRef (ссылочный тип хранится в отдельной сборке), и 2 - для TypeSpec (ссылочный тип является обобщенным типом, массивом, и т.д., смотрите раздел 4.9 TypeSpec). Остальные биты кодируют индекс строки: обратите внимание, что индексы начинается с единицы, то есть первая строка в любой таблице метаданных всегда имеет индекс 1, а не 0.
Пример 1
В этом примере объявлено одно поле с прикрепленным к нему пользовтельским обязательным модификатором, modreq принимат в качестве аргумента тип TestClass, объявленный в той же сборке, как показано ниже.
// Полный исходник: TypeDefOrRefEncoded\1.il
// Двоичный файл: TypeDefOrRefEncoded\1.dll
// (...)
.class public TestClass extends [mscorlib]System.Object { }
.field public int64 modreq(TestClass) TestField
FieldSig для вышеприведенного примера кода выглядит так:
Смещение |
Значение |
Что означает |
|
|
размер сигнатуры |
|
|
|
|
|
встреченный пользовательский обязательный модификатор ( |
|
|
Элемент |
|
|
тип поля ( |
2.3 Param
Этот элемент описывает один параметр, заданный в методе или свойстве, и оттого входящий в состав PropertySig, MethodDefSig, MethodRefSig, и т.д. Ниже приведена схема синтаксиса для элемента Param:
Рисунок 4. Схема синтаксиса элемента Param
Пример 1
В методе TestMethod, показанном ниже, есть два пользовательских модификатора, прикрепленных к одному параметру. Цель данного примера - показать формат элемента Param и снова показать, как работает элемент TypeDefOrRefEncoded.
// Полный исходник: Param\1.il
// Двоичный файл: Param\1.dll
// (...)
.class public TestClass extends [mscorlib]System.Object { }
.method public static void TestMethod(int32 modopt(TestClass) modreq([mscorlib]System.Runtime.CompilerServices.IsLong) Param1)
{
ret
}
Соответствующая сигнатура MethodDefSig для этого метода такова:
Смещение |
Значение |
Что означает |
|
|
размер сигнатуры |
|
|
метод |
|
|
количество параметров |
|
|
тип возвращаемого значения ( |
|
|
встреченный пользовательский обязательный модификатор ( |
|
|
Ссылочный ряд равен |
|
|
встреченный пользовательский необязательный модификатор ( |
|
|
Ссылочный ряд равен |
|
|
тип первого параметра ( |
2.4 RetType
Этот элемент почти идентичен элементу Param, он имеет еще один дополнительный путь, содержащий тип VOID. Поскольку нижеприведенная схема синтаксиса для этого элемента понятна без пояснений, в данном подразделе нет примеров.
Рисунок 5. Схема синтаксиса элемента RetType
2.5 Type
Неудивительно, что элемент Type описывает тип, и не только простой тип (такой как int32, bool, string, и т.д.), но и массивы, обобщенные типы экземпляров и сложные типы (классы и структуры). Листинг ниже показывает схему синтаксиса для этого элемента. Разумеется, слова, написанные в верхнем регистре, являются константами, значения которых можно найти в таблице констант в первой части. Константа GENERICINST входит в состав данного элемента, но помните, что сигнатуры TypeSpec, MethodSpec и MethodDefSig имеют разные цели.
Type ::=
BOOLEAN | CHAR | I1 | U1 | I2 | U2 | I4 | U4 | I8 | U8 | R4 | R8 | I | U |
| ARRAY Type ArrayShape
| CLASS TypeDefOrRefEncoded
| FNPTR MethodDefSig
| FNPTR MethodRefSig
| GENERICINST (CLASS | VALUETYPE) TypeDefOrRefEncoded GenArgCount Type *
| MVAR number
| OBJECT
| PTR CustomMod* Type
| PTR CustomMod* VOID
| STRING
| SZARRAY CustomMod* Type
| VALUETYPE TypeDefOrRefEncoded
| VAR number
Пример 1
Посмотрим, что случится с сигнатурой MethodDefSig, если метод принимает обобщенные типы как нормальные параметры.
// Полный исходник: Type\1.cs
// Двоичный файл: Type\1.dll
// (...)
public class TestClass<GenArg1, GenArg2> { }
public class TestRunClass
{
public void TestRunMethod()
{
TestMethod(new TestClass<int, string>());
}
public void TestMethod(TestClass<int, string> Param1) { }
}
Разбор сигнатуры MethodDefSig для метода TestMethod.
Смещение |
Значение |
Что означает |
|
|
размер сигнатуры |
|
|
Метод является методом экземпляра. |
|
|
количество нормальных параметров |
|
|
тип возвращаемого значения ( |
|
|
тип первого параметра – обобщенный тип ( |
|
|
тип первого параметра - обобщённый класс ( |
2.6 ArrayShape
Многие люди, использующие платформу .NET, знают, что массив может иметь более одной размерности, но не знают, что каждая размерность в массиве может иметь нижнюю границу. Вероятно, причина в том, что большинство разработчиков использует язык C#, не позволяющий использовать нижние границы, за исключением использования метода Array.CreateInstance для создания такого типа массива. Элемент ArrayShape хранит полное определение многомерного массива, он хранит число размерностей, размер и нижнюю границу каждой размерности, имеющиеся у массива. Ниже приведена схема синтаксиса для данного элемента вместе с кратким описанием, скопированным из спецификации.
Рисунок 6. Схема синтаксиса элемента ArrayShape
Rank - целое (хранится в сжатой форме, смотрите §23.2), задающее число размерностей в массиве (должно равняться 1 или больше). NumSizes – сжатое целое, говорящее, сколько размерностей имеют заданные размеры (должно равняться 0 или больше). Size – сжатое целое, задающее размер этой размерности – последовательность начинается с первой размерности и продолжается до полного числа элементов NumSizes. Аналогично, NumLoBounds – сжатое целое, говорящее, сколько размерностей имеют заданные нижние границы (должно равняться 0 или больше). И LoBound - сжатое целое, задающее нижнюю границу этой размерности - последовательность начинается с первой размерности и продолжается до полного числа элементов NumLoBounds. Нельзя пропускать никакую размерность в этих двух последовательностях, но число заданных размерностей может быть меньше, чем Rank.
Замечание: Не путайте многомерные массивы с неровными массивами. Многомерный массив в CIL может быть, например: int32[,], а неровный массив является int32[][]. Также заметьте, что ArrayShape хранит информацию только о многомерных массивах. Одномерный массив обозначается константой SZARRAY – вот и все (смотрите элемент Type). Дополнительные сведения о массивах в .NET читайте в статье Типы массивов в .NET в журнале MSDN.
Важно: К сожалению, как показывает второй пример, компилятор ILASM имеет ряд проблем с обработкой нижних границ массивов (поле LoBound на рисунке 6), нижняя граница умножается на два. Это неправильно, так как спецификация говорит, что нижние границы должны храниться в сигнатурах без внесения изменений. Ниже приведена таблица, взятая из спецификации, показывающая примеры объявлений массивов и их правильных параметров в элементе ArrayShape. Более того, спецификация не оговаривает, в каком случае(ях) поля NumSizes и NumLoBounds могут быть меньше, чем поле Rank. По нашим наблюдениям, поля NumSizes и NumLoBounds меньше, чем Rank, лишь в одном случае – когда нижняя граница не задана для всех размерностей (это показано во второй строке в таблице ниже), иначе NumSizes и NumLoBounds всегда равны Rank, это противоречит третьей и пятой строке в таблице ниже.
Объявление |
Type |
Rank |
NumSizes |
Size |
NumLoBounds |
LoBound |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
Пример 1
Посмотрим, как ArrayShape работает на практике.
// Полный исходник: ArrayShape\1.il
// Двоичный файл: ArrayShape\1.dll
// (...)
.field public int32[,,] TestField
Следующая сигнатура FieldSig должна генерироваться вышеприведенным многомерным массивом.
Смещение |
Значение |
Что означает |
|
|
размер сигнатуры |
|
|
|
|
|
значение типа поля - |
|
|
тип массива - |
|
|
количество размерностей массива (поле |
|
|
размер размерностей массива не задан (поле |
|
|
Нижние границы размерностей массива не заданы (поле |
Пример 2
Этот пример показывает, как элемент ArrayShape ведет себя при объявлении многомерных массивов с заданными нижними границами.
// Полный исходник: ArrayShape\2.il
// Двоичный файл: ArrayShape\2.dll
// (...)
.field public int32[0...5,,4...6] TestField
Вся сигнатура FieldSig выглядит так:
Смещение |
Значение |
Что означает |
|
|
размер сигнатуры |
|
|
|
|
|
значение типа поля - |
|
|
тип массива - |
|
|
количество размерностей массива (поле |
|
|
количество размеров для этого массива (поле |
|
|
размер первой размерности массива (поле |
|
|
размер второй размерности массива, ноль означает – не задан (поле |
|
|
размер третей размерности массива (поле |
|
|
количество нижних границ для этого массива (поле |
|
|
нижняя граница первой размерности массива (поле |
|
|
нижняя граница второй размерности массива (поле |
|
|
нижняя граница третьей размерности массива (поле |
Пример 3
Теперь посмотрим, как элемент ArrayShape выглядит в реальности, и сравним результаты со спецификацией.
// Полный исходник: ArrayShape\3.il
// Двоичный файл: ArrayShape\3.dll
// (...)
.field public int32[0...2] TestField
Да, NumLoBounds равняется Rank, несмотря на то, что спецификация говорит, что NumLoBounds должен равняться нулю.
Смещение |
Значение |
Что означает |
|
|
размер сигнатуры |
|
|
|
|
|
значение типа поля - |
|
|
тип массива - |
|
|
количество размерностей массива (поле |
|
|
количество размеров для этого массива (поле |
|
|
размер первой размерности массива (поле |
|
|
количество нижних границ для этого массива (поле |
|
|
нижняя граница первой размерности массива (поле |
3. Вывод
Как видно, сигнатуры являются сложным уродством, но делают исполняемый файл .NET маленьким, компактным и логичным.