Формат файла .NET – внутренняя структура сигнатур, часть 2 из 2 - Сигнатуры

ОГЛАВЛЕНИЕ

1. Сигнатуры (продолжение)

Здесь приведено продолжение первой части.

1.1 LocalVarSig

Сигнатура LocalVarSig также индексируется столбцом StandAloneSig.Signature, он хранит тип всех локальных переменных, выделенных во время выполнения метода. Элемент LOCAL_SIG является прологом сигнатуры и имеет постоянное значение 0x07, элемент Count является сжатым беззнаковым целым, хранящим количество локальных переменных, принадлежащих связанному с ними методу, элемент BYREF является сокращением константы ELEMENT_TYPE_BYREF (смотрите константы в первой части) и показывает, что элемент Type(тип) указывает на реальную переменную. Элемент Constraint(ограничение) показывает, что целевой тип не будет перемещаться сборщиком мусора при выполнении восстановления памяти, так как локальные переменные находятся в стеке (где сборщик мусора не выполняет никаких действий), Type(тип) переменной должен быть ссылочным типом (как System.Object – выделяется в куче) или типом значения (как System.Decimal – выделяется в стеке), но если целевой тип (закрепленный) является типом значения, его определение должно содержать элемент BYREF, в данном случае ссылка на переменную хранится в стеке, но сама переменная выделяется в куче. Более подробно о закреплении рассказано здесь. На рисунке 1 ниже показана полная схема синтаксиса для этой сигнатуры.

Обратите особое внимание на элемент TYPEDBYREF на схеме ниже: это типизированная ссылка, она содержит не только управляемый указатель (как нормальная ссылка) на адрес, но и динамическое представление данных. Процитируем описание элемента из спецификации: "Сигнатура локальной переменной типизированной ссылки указывает, что локальная переменная будет содержать управляемый указатель на адрес и динамическое представление типа, хранящиеся по этому адресу. Сигнатура типизированной ссылки похожа на константу byref, но тогда как byref определяет тип как часть ограничения byref (и, стало быть, статически как часть описания типа), типизированная ссылка динамически предоставляет информацию о типе. Типизированная ссылка, по сути, является полной сигнатурой и не сочетается с другими ограничениями. В частности, невозможно задать byref, тип которого является типизированной ссылкой".

Типизированная ссылка также очень помогает при передаче byref распакованных данных (т.е. данные, хранящиеся в стеке, всегда являются типами значений) в методы, не ограниченные статически принимаемым ими типом и требующие, наряду с передачей управляемого указателя на адрес, статического типа адреса: типизированная ссылка удовлетворяет эти потребности. Заметьте, что параметр «типизированная ссылка» также может ссылаться на адрес, находящийся в стеке, и время жизни этого адреса будет ограничено временем выполнения метода (в течение которого выделяется типизированная ссылка), а значит, компилятор CIL проверяет время жизни byref и параметра типизированной ссылки (подробней читайте в §12.4.1.5.2 в спецификации ECMA-355). Типизированная ссылка представлена в .NET BCL (библиотека базовых классов) в виде структуры TypedReference.

 

Рисунок 1. Схема синтаксиса сигнатуры LocalVarSig

Пример 1

Данный пример показывает объявление типов значений byref в стеке (только), пример кода написан на языке CIL (общий промежуточный язык) и выглядит так.

// Полный исходник: LocalVarSig\1.il
// Двоичный файл: LocalVarSig\1.dll
// (...)

.method public static void TestMethod()
{
    .locals init(int32 &IntVarByRef)
    ret
}

Сигнатура LocalVarSig для этого примера кода приведена в таблице ниже.

Смещение

Значение

Что означает

0x05

0x04

размер сигнатуры

0x06

0x07

пролог сигнатуры (константа LOCAL_SIG).

0x07

0x01

Общее число переменных, объявленных в этом методе, равняется одной.

0x08

0x10

Так как фактическая переменная хранится в динамической куче, присутствует элемент BYREF значения 0x10.

0x09

0x08

Тип переменной (int32), смотрите константы в первой части.

Пример 2

Пример ниже показывает, что происходит с сигнатурой при использовании типизированной ссылки: в начале объявляется переменная IntVar, в следующей строке получается типизированная ссылка с помощью ключевого слова __makeref (недокументировано и не совместимо с CLS) и сохраняется в переменной TypedByRefVar.

// Полный исходник: LocalVarSig\2.cs
// Двоичный файл: LocalVarSig\2.dll
// (...)

[CLSCompliant(false)]
public void TestMethod()
{
    int IntVar = 0;
    TypedReference TypedByRefVar = __makeref(IntVar);
}

LocalVarSig для этого примера выглядит, как показано ниже.

Смещение

Значение

Что означает

0x1E

0x04

размер сигнатуры

0x1F

0x07

пролог сигнатуры (константаLOCAL_SIG)

0x20

0x02

Общее число переменных, объявленных в этом методе, равняется двум.

0x21

0x08

Тип первой переменной (int32), смотрите константы в первой части.

0x22

0x16

Тип второй переменной (TYPEDBYREF), смотрите константы в первой части.

Пример 3

Рассмотрим немного более сложный пример: в этом примере кода создается класс TestDataClass, имеющий лишь один член по имени StringVarToBePinned типа string(строка). В методе TestMethod (помеченный как unsafe) создается экземпляр класса TestDataClass, в строке ниже закрепляется член StringVarToBePinned и ссылка на него присваивается указателю FixedVar с помощью ключевого слова fixed. Такая обработка гарантирует, что между фигурными скобками {и} член dataClass.StringVarToBePinned не будет перемещаться действиями сборщика мусора, а значит, указатель FixedVar на член всегда будет действителен внутри фигурных скобок ключевого слова fixed. В методе нельзя непосредственно объявить переменную, которая будет закреплена, потому что такое значение уже закреплено (помещено в стек), поэтому переменная должна быть обернута классом TestDataClass (помещенным в кучу).

// Полный исходник: LocalVarSig\3.cs
// Двоичный файл: LocalVarSig\3.dll
// компилировать с параметром "/опасный"
// (...)

public class TestDataClass
{
    public string StringVarToBePinned;
}

public class TestClass
{
    public unsafe void TestMethod()
    {
        TestDataClass dataClass = new TestDataClass();
        fixed (char* FixedVar = dataClass.StringVarToBePinned) { }
    }
}

Данный пример сложен еще по одной причине: в какой-то момент он использует еще не описанный элемент, то есть TypeDefOrRefEncoded, этот элемент определяет, в какой строке и в какой таблице метаданных (TypeDef, TypeRef или TypeSpec) описан заданный тип. Здесь эти элементы детально не разбираются, если хотите –  перейдите сразу к описанию этого элемента в подразделе 5.2 TypeDefOrRefEncoded в следующем разделе. LocalVarSig для вышеприведенного кода рассмотрена в таблице ниже.

Смещение

Значение

Что означает

0x20

0x08

размер сигнатуры

0x21

0x07

пролог сигнатуры (константаLOCAL_SIG)

0x22

0x03

Общее число переменных, объявленных в этом методе, равняется трем.

0x23

0x12

Тип первой переменной (CLASS - за которым следует элемент TypeDefOrRefEncoded), смотрите константы в первой части.

0x24

0x08

Тип первой переменной, являющийся классом TestDataClass, описан в таблице метаданных TypeDef в строке 2, который является классом TestDataClass. Элемент TypeDefOrRefEncoded не объяснен в текущей главе.

0x25

0x0F

Тип второй переменной (PTR - за которым следует элементType), смотрите константы в первой части.

0x26

0x03

Тип указателя из предыдущего байта (char – в конце это char*), смотрите константы в первой части.

0x27

0x45

Третья переменная закреплена, смотрите константы.

0x28

0x0E

Тип третьей закрепленной переменной (string), смотрите константы.

1.2 CustomAttrib

Данная сигнатура хранит экземпляры пользовательских атрибутов, но немного отличается от рассмотренных ранее сигнатур. Главное отличие заключается в том, что CustomAttrib ,в отличие от, например, сигнатуры MethodRefSig, хранит значения параметров, заданных пользовательскому атрибуту, и не хранит типы параметров. То есть сигнатура CustomAttrib хранит лишь значения параметров (фиксированных и именованных), заданных при создании экземпляра пользовательского атрибута; информация об их типах и количестве не дублируется в сигнатуре. Сигнатура индексируется столбцом CustomAttribute.Value, столбец Parent(родитель) указывает, в какой таблице (TypeDef – для типа, MethodDef – для метода, и т.д.) и в какой строке описан приписанный элемент (метод, тип и т.д.). Есть и второе существенное различие по сравнению с другими сигнатурами: в сигнатуре CustomAttrib все двоичные значения хранятся с несжатым прямым порядком байтов, исключая элемент PackedLen (рассмотрен ниже) и размер сигнатуры. Не путайте пользовательский атрибут с пользовательским модификатором. Полная схема синтаксиса  состоит из 4 частей, рассмотрим первую.

 

Рисунок 2a. Схема синтаксиса сигнатуры CustomAttrib

Пока она весьма простая, начинается с Prolog, имеющего постоянное значение 0x0001 и занимающего два байта (unsigned int16 – несжатый и с прямым порядком байтов). Затем идут фиксированные аргументы (FixedArg показан на рисунке 2b), их количество и типы могут быть получены путем изучения соответствующей строки конструктора в таблице метаданных MethodDef или MemberRef (если класс атрибута хранится в другой сборке): обратите внимание, что метод vararg нельзя использовать в качестве конструктора атрибута. Далее следует количество именованных параметров (NumNamed – двухбайтовый unsigned int16 - несжатый и с прямым порядком байтов), и в конце расположены сами именованные параметры, повторенные NumNamed раз.

 

Рисунок 2b. Схема синтаксиса сигнатуры CustomAttrib

Эта схема немного сложнее предыдущей, но тоже весьма простая: верхний путь на схеме показывает, что параметр не является одномерным массивом с нулевой базой (SZARRAY, смотрите константы в первой части); нижний путь показывает путь SZARRAY, являющийся массивом; количество элементов в массиве SZARRAY хранится в элементе NumElem типа int32 (несжатый и с прямым порядком байтов), занимающем 4 байта; если параметр SZARRAY является null(пустой), то NumNamed задано значение 0xFFFFFFFF. CLI разрешает использовать только одномерные массивы с нижней границей нуля (SZARRAY), одномерный массив с нулевой базой типа int32 является int32[], но не int32[,,] и также не int32[3...8]. Чтобы узнать больше о массивах в .NET, прочитайте статью Типы массивов в .NET из журнала MSDN.

 

Рисунок 2c. Схема синтаксиса сигнатуры CustomAttrib

Пожалуй, эта часть –  самая непонятная из всех четырех. Формат, принимаемый Elem, зависит от следующих условий (взято из спецификации).

Если тип параметра простой (первая линия на схеме выше) (bool, char, float32, float64, int8, int16, int32, int64, unsigned int8,unsigned int16, unsigned int32 или unsigned int64), то 'блоб' содержит его двоичное значение (Val). (bool – один байт со значением 0 (false) или 1 (true); char – двухбайтный символ Юникод; а остальные имеют свой очевидный смысл.) Данная схема также используется, если тип параметра – enum – хранит значение целого типа, лежащего в основе enum(перечисление).

Если тип параметра - строка (вторая линия на схеме выше), то блоб содержит SerString – количество байтов PackedLen (сжатый и с обратным порядком байтов – добавлено автором), за которым следуют символы UTF8. Если строка равна null, ее PackedLen имеет значение 0xFF (без следующих символов). Если строка пустая (""), то PackedLen имеет значение 0x00 (без следующих символов).

Если тип параметра - System.Type (смотрите ключевое слово typeof – добавлено автором статьи) (тоже вторая линия на схеме выше), его значение хранится как SerString (как определено в предыдущем абзаце), представляющее его каноническое имя. Каноническое имя содержит сборку, где оно определено, его версию, культуру и маркер открытого ключа. Если имя сборки не указано, CLI сначала ищет в текущей сборке, а затем в системной библиотеке (mscorlib); в этих двух особых случаях разрешено не указывать имя сборки, версию, культуру и маркер открытого ключа.

Если тип параметра - System.Object (третья линия на схеме выше), хранимое значение является "упакованным" экземпляром этого типа значения. В данном случае блоб содержит фактический FieldOrPropType типа (смотрите ниже), с последующим распакованным значением аргумента. [Примечание: нельзя передать значение null в этом случае.]

 

Рисунок 2d. Схема синтаксиса сигнатуры CustomAttrib

Последняя часть показывает формат элемента NamedArg, являющегося именованным аргументом (полем или свойством). Так как поля и свойства могут иметь одинаковое имя, первым элементом является FIELD, имеющее постоянное однобайтовое значение 0x53, если именованный параметр ссылается на поле или PROPERTY, имеющее постоянное однобайтовое значение 0x54, если именованный параметр ссылается на свойство. Далее идет элемент FieldOrPropType, описывающий тип именованного свойства или поля в одном или двух байтах: если тип именованного параметра – распакованный простой тип значения (определен выше), то  FieldOrPropType должен содержать ровно одно постоянное значение связанного с ним типа (BOOLEAN, CHAR, I1, U1, I2, U2, I4, U4, I8, U8, R4, R8, STRING – смотрите таблицу констант в первой части), но если тип именованного параметра – упакованный простой тип значения, то перед элементом FieldOrPropType стоит байт, содержащий значение 0x51, в этом случае FieldOrPropType занимает два байта. Элемент FieldOrPropName является SerString (объяснен выше), содержащим имя свойства или поля. В конце идет один элемент FixedArg, показанный выше. Элемент NamedArg является нормальным FixedArg, перед которым стоит некоторая дополнительная информация, указывающая, какое поле или свойство он обозначает.

Пример 1

Данный пример показывает формат элемента SerString и как CustomAttrib различает поля и свойства, служащие именованными параметрами. В примере ниже имеется атрибут TestAttribute, требующий передачи одного фиксированного параметра Fixed1 типа int32, дополнительно можно (и это делается) передать два дополнительных именованных параметра типа int16 и string, как показано в куске кода ниже.

// Полный исходник: CustomAttrib\1.cs
// Двоичный файл: CustomAttrib\1.dll
// (...)

[AttributeUsage(AttributeTargets.Class)]
public class TestAttribute : Attribute
{
    public TestAttribute(int Fixed1) { }

    public short Named1 { get; set; }

    public string Named2;
}

[Test(1, Named1 = 1, Named2 = "Abcd")]
public class TestClass { }

Полная сигнатура CustomAttrib для этого случая имеет длину 33 байта, поэтому в некоторых точках несколько байтов были объединены в одну строку с общим описанием.

Смещение

Значение

Что означает

0x3E

0x21

Размер сигнатуры хранится в виде сжатого целого с обратным порядком байтов.

0x3F
0x40

0x01
0x00

Пролог хранится в виде несжатого значения unsigned int16 0x0001 с прямым порядком байтов.

0x41
0x42
0x43
0x44

0x01
0x00
0x00
0x00

Значение первого фиксированного аргумента атрибута (Fixed1) равняется 0x00000001и хранится в виде несжатого int32 с прямым порядком байтов. Это показывает верхняя линия на рисунке 2b и первый путь на рисунке 2c.

0x45
0x46

0x02
0x00

Количество именованных параметров, заданных в атрибуте, обозначено элементом NumNamed на рисунке 2a и хранится в виде unsigned int16с прямым порядком байтов. Задано ровно два необязательных параметра, и значение этого двухбайтного элемента равно 0x0002.

0x47

0x54

Значение этого байта показывает, что целевой именованный параметр представлен свойством (смотрите константы в первой части), это элемент PROPOERTY на рисунке 2d.

0x48

0x06

Тип целевого свойства (int16, смотрите константы в первой части). ЭтотбайтпредставленэлементомFieldOrPropType на рисунке 2d.

0x49
0x4A
0x4B
0x4C
0x4D
0x4E
0x4F

0x06
0x4E
0x61
0x6D
0x65
0x64
0x31

Это строка SerString, задающая имя целевого свойства (представлено элементом FieldOrPropName на рисунке 2d). SerString – нормальная строка юникод, перед которой стоит ее размер в байтах, размер хранится как сжатое целое с обратным порядком байтов. Строка занимает 6 байтов (смещение 0x49), так как имя строки содержит только символы из таблицы ASCII, каждый один символ занимает ровно один байт, можно легко прочитать текст строки Named1.

0x50
0x51

0x01
0x00

Значение первого именованного аргумента атрибута (Named1) равняется 0x00001и хранится в виде несжатого int16 с прямым порядком байтов. Это показывает верхняя линия на рисунке 2b и первый путь на рисунке 2c.

0x52

0x53

Значение этого байта показывает, что целевой именованный параметр представлен полем (смотрите константы в первой части), это элемент FIELD на рисунке 2d.

0x53

0x0E

Тип целевого поля (string, смотрите константы в первой части). ЭтотбайтпредставленэлементомFieldOrPropType на рисунке 2d.

0x54
0x55
0x56
0x57
0x58
0x59
0x5A

0x06
0x4E
0x61
0x6D
0x65
0x64
0x32

Это вновь строка SerString, задающая имя целевого свойства (представлено элементом FieldOrPropName на рисунке 2d). Длина этой строки – 6 байтов (смотрите на смещение 0x54), остальные байты похожи на предыдущую строку, отличается лишь последний байт, текстом строки является Named2, смотрите таблицу ASCII.

0x5B
0x5C
0x5D
0x5E
0x5F

0x04
0x41
0x62
0x63
0x64

Значение второго именованного аргумента атрибута (Named2) равняется Abcd (смотрите таблицу ASCII) и хранится как SerString. Это показывает верхняя линия на рисунке 2b и средний путь на рисунке 2c. Поскольку 0x5F - 0x3E = 0x21, т.е. конечное смещение – первое смещение = размер сигнатуры, сигнатура заканчивается здесь.

Пример 2

В этом примере показан формат сигнатуры при использовании System.Type, SZARRAY и упакованных типов значений в качестве аргументов атрибута TestAttribute, определенного ниже.

// Полный исходник: CustomAttrib\2.cs
// Двоичный файл: CustomAttrib\2.dll
// (...)

[AttributeUsage(AttributeTargets.Class)]
public class TestAttribute : Attribute
{
    public TestAttribute(object Param1, int[] Param2, Type Param3) { }
}

[Test(1, new int[] {1, 2, 3}, typeof(string))]
public class TestClass { }

Как и в прошлом примере, сигнатура очень длинная (занимает 116 байтов), поэтому она разбита на более мелкие части.

Смещение

Значение

Что означает

0x2B

0x74

Размер сигнатуры хранится в виде сжатого целого с обратным порядком байтов.

0x2C
0x2D

0x01
0x00

Пролог хранится в виде несжатого значенияunsigned int16 0x0001 с прямым порядком байтов.

0x2E

0x08

Тип первого фиксированного аргумента (int32– упакован внутри System.Object). Этот случай обозначен третьим путем на рисунке 2c, где сразу перед значением стоит тип значения.

0x2F
0x30
0x31
0x32

0x01
0x00
0x00
0x00

Значение, тип которого был задан в предыдущем байте, так как типом значения является int32, занимающий ровно 4 байта. Оно хранится с прямым порядком байтов, поэтому значение равно 0x00000001.

0x33
0x34
0x35
0x36

0x03
0x00
0x00
0x00

Далее идет определение второго параметра, так как второй аргумент является одномерным массивом с нулевой базой (SZARRAY). Эти 4 байта задают количество элементов, заданных в массиве второго параметра, это значение хранится как unsigned int32с прямым порядком байтов.

0x37
0x38
0x39
0x3A

0x01
0x00
0x00
0x00

Значение первого элемента массива во втором параметре, длиной 4 байта, так как тип массива - int32, значение - 0x00000001.

0x3B
0x3C
0x3D
0x3E

0x02
0x00
0x00
0x00

Значение второго элемента массива во втором параметре, длиной 4 байта, так как тип массива -int32, значение - 0x00000002.

0x3F
0x40
0x41
0x42

0x03
0x00
0x00
0x00

Значение третьего элемента массива во втором параметре, длиной 4 байта, так как тип массива - int32, значение - 0x00000003.

0x43
0x44
0x45
0x46
0x47
0x48
0x49
0x4A
0x4B
0x4C
0x4D
0x4E
0x4F
0x50
0x51
0x52
0x53
0x54
0x55
0x56
0x57
0x58
0x59
0x5A
0x5B
0x5C
0x5D
0x5E
0x5F
0x60
0x61
0x62
0x63
0x64
0x65
0x66
0x67
0x68
0x69
0x6A
0x6B
0x6C
0x6D
0x6E
0x6F
0x70
0x71
0x72
0x73
0x74
0x75
0x76
0x77
0x78
0x79
0x7A
0x7B
0x7C
0x7D
0x7E
0x7F
0x80
0x81
0x82
0x83
0x84
0x85
0x86
0x87
0x88
0x89
0x8A
0x8B
0x8C
0x8D
0x8E
0x8F
0x90
0x91
0x92
0x93
0x94
0x95
0x96
0x97
0x98
0x99
0x9A
0x9B
0x9C
0x9D

0x5A
0x53
0x79
0x73
0x74
0x65
0x6D
0x2E
0x53
0x74
0x72
0x69
0x6E
0x67
0x2C
0x20
0x6D
0x73
0x63
0x6F
0x72
0x6C
0x69
0x62
0x2C
0x20
0x56
0x65
0x72
0x73
0x69
0x6F
0x6E
0x3D
0x32
0x2E
0x30
0x2E
0x30
0x2E
0x30
0x2C
0x20
0x43
0x75
0x6C
0x74
0x75
0x72
0x65
0x3D
0x6E
0x65
0x75
0x74
0x72
0x61
0x6C
0x2C
0x20
0x50
0x75
0x62
0x6C
0x69
0x63
0x4B
0x65
0x79
0x54
0x6F
0x6B
0x65
0x6E
0x3D
0x62
0x37
0x37
0x61
0x35
0x63
0x35
0x36
0x31
0x39
0x33
0x34
0x65
0x30
0x38
0x39

SerString длиной 90 байтовописываетканоническоеимятипа, заданноевтретьемпараметре, имеетследующеезначениеSystem.String, mscorlib, Version(версия)=2.0.0.0, Culture(культура)=neutral(нейтральная), PublicKeyToken=b77a5c561934e089. Она показана средним путем на рисунке 2c

0x9E
0x9F

0x00
0x00

Два заключительных байта, не входящие в предыдущую SerString(0x9F - 0x44 != 0x5A), но являющиеся частью всего CustomAttrib (0x9F - 0x2C = 0x74) и не содержащие никаких данных. Вероятно, каноническое имя имеет некое выравнивание, и поэтому данные нули присутствуют. К сожалению, спецификация ничего не говорит о них.

1.3 MethodSpec

Сигнатура MethodSpec простая, она описывает каждую реализацию обобщенного метода, индексируется столбцом MethodSpec.Signature, и ее синтаксис имеет следующий вид, начинаясь с пролога GENRICINST (видите ли вы отсутствующую "E" ?), имеющего однобайтное значение 0x0A (значение этой константы отличается от значения константы ELEMENT_TYPE_GENERICINST, определенной в таблице констант в первой части), где Type повторяется GenArgCount раз.

MethodSpecBlob ::=
   GENRICINST GenArgCount Type Type*

Пример 1

В примере ниже реализован обобщенный метод TestMethod с заданием трех обобщенных аргументов.

// Полный исходник: MethodSpec\1.cs
// Двоичный файл: MethodSpec\1.dll
// (...)

public class TestClass
{
    public void TestMethod<GenArg1, GenArg2, GenArg3>() { }
}

public class TestRunClass
{
    public void TestRunMethod()
    {
        new TestClass().TestMethod<short, int, string>();
    }
}

MethodSpec для этого случая выглядит так:

Смещение

Значение

Что означает

0x18

0x05

размер сигнатуры

0x19

0x0A

пролог

0x1A

0x03

Количество обобщенных аргументов, заданных в обобщенном методе.

0x1B

0x06

Тип первого параметра (int16), смотрите константы в первой части.

0x1C

0x08

Тип второго параметра (int32), смотрите константы в первой части.

0x1D

0x0E

Тип третьего параметра (string), смотрите константы в первой части.

1.4 TypeSpec

Сигнатура TypeSpec индексируется столбцом TypeSpec.Signature и используется при: реализации типа в виде многомерного массива; реализации типа в виде одномерного массива, перед которым стоит пользовательский модификатор(ы); реализации обобщенного типа и иных действиях, как показано на схеме ниже. Поскольку некоторые элементы еще не объяснены (такие как пользовательские модификаторы, формы массивов), используется лишь ограниченная функциональность сигнатуры TypeSpec. В следующей главе рассматриваются элементы CustomMod, ArrayShape, TypeDefOrRefEncoded, и мы вернемся к сигнатуре TypeSpec и воспользуемся остальными возможностями сигнатуры. Также заметьте, что в отличие от предыдущего примера, где константа/пролог GENRICINST (отсутствует "E") также используется, в TypeSpec используется константа ELEMENT_TYPE_GENERICINST, определенная в таблице общих констант (в первой части статьи).

TypeSpecBlob ::=
  PTR      CustomMod*  VOID
| PTR      CustomMod*  Type
| FNPTR    MethodDefSig
| FNPTR    MethodRefSig
| ARRAY    Type  ArrayShape
| SZARRAY  CustomMod*  Type
| GENERICINST (CLASS | VALUETYPE) TypeDefOrRefEncoded GenArgCount Type Type*

Пример 1

В этом примере реализован обобщенный тип TypeSpec, как показано в куске кода ниже.

// Полный исходник: TypeSpec\1.cs
// Двоичный файл: TypeSpec\1.dll
// (...)

public class TestClass<GenArg1, GenArg2> { }

public class TestRunClass
{
    public void TestRunMethod()
    {
        TestClass<int, string> TestVar = new TestClass<int, string>();
    }
}

TypeSpec для этого случая выглядит так:

Смещение

Значение

Что означает

0x13

0x06

размер сигнатуры

0x14

0x15

константа ELEMENT_TYPE_GENERICINST, смотрите таблицу констант в первой части.

0x15

0x12

тип обобщенного типа (CLASS), смотрите таблицу констант в первой части.

0x16

0x08

Реализованный обобщенный тип описан в таблице метаданных TypeDef в строке 2, элемент TypeDefOrRefEncoded не объяснен в текущей главе.

0x17

0x02

Количество обобщенных аргументов, заданных в типе, равно двум.

0x18

0x08

тип первого обобщенного параметра (int32), смотрите константы в первой части.

0x19

0x0E

тип второго обобщенного параметра (string), смотрите константы в первой части.

1.5 MarshalSpec

Сигнатура MarshalSpec генерируется при использовании атрибута MarshalAs в полях, параметрах и возвращаемых параметрах. Она задает, как данные должны преобразовываться при вызове из/в неуправляемый код через платформозависимый вызов. Сигнатура индексируется в столбце FieldMarshal.NativeType ( имя таблицы метаданных неверно, в действительности не важно, MarshalSpec описывает поле, параметр или возвращаемый параметр), она всегда индексируется вышеназванным столбцом. Элементы ParamNum и NumElem в листинге синтаксиса ниже описывают, соответственно, параметр в вызове метода, дающий количество элементов в массиве, количество элементов или дополнительных элементов. И те, и другие элементы хранятся в сигнатуре как сжатые целые, их цель – помогать вычислить полный размер в байтах, занимаемый массивом в памяти. Специфичная для Microsoft реализация дескриптора преобразования богаче, чем описано здесь, и использует дополнительные константы и расширенный синтаксис. Дополнительные сведения о реализации Microsoft MarshalSpec смотрите в спецификации метаданных часть II - раздел §23.4.

MarshalSpec ::=
  NativeIntrinsic
| ARRAY ArrayElemType
| ARRAY ArrayElemType ParamNum
| ARRAY ArrayElemType ParamNum NumElem

ArrayElemType ::=
   NativeIntrinsic

NativeIntrinsic ::=
  BOOLEAN | I1 | U1 | I2 | U2 | I4 | U4 | I8 | U8 | R4 | R8
| LPSTR | LPSTR | INT | UINT | FUNC

Для вычисления размера массива в байтах применяется следующий псевдокод, где @ParamNum означает значение, переданное для номера параметра ParamNum.

if ParamNum = 0
   SizeInBytes = NumElem * sizeof (elem)
else
   SizeInBytes = ( @ParamNum +  NumElem ) * sizeof (elem)
endif

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

Имя

Значение

NATIVE_TYPE_BOOLEAN

0x02

NATIVE_TYPE_I1

0x03

NATIVE_TYPE_U1

0x04

NATIVE_TYPE_I2

0x05

NATIVE_TYPE_U2

0x06

NATIVE_TYPE_I4

0x07

NATIVE_TYPE_U4

0x08

NATIVE_TYPE_I8

0x09

NATIVE_TYPE_U8

0x0A

NATIVE_TYPE_R4

0x0B

NATIVE_TYPE_R8

0x0C

NATIVE_TYPE_LPSTR

0x14

NATIVE_TYPE_LPWSTR

0x15

NATIVE_TYPE_INT

0x1F

NATIVE_TYPE_UINT

0x20

NATIVE_TYPE_FUNC

0x26

NATIVE_TYPE_ARRAY

0x2A

NATIVE_TYPE_MAX

0x50

Пример 1

Начнем с простейшего примера, показанного в листинге кода ниже.

// Полный исходник: MarshalSpec\1.cs
// Двоичный код: MarshalSpec\1.dll
// (...)

[MarshalAs(UnmanagedType.LPWStr)]
public string TestField;

Этот код сгенерировал следующую сигнатуру MarshalSpec.

Смещение

Значение

Что означает

0x1C

0x01

размер сигнатуры

0x1D

0x15

Поле TestField преобразуется вLPWSTRв неуправляемом коде.

Пример 2

Пришло время для более сложного примера –  мы преобразуем массив типа int32 в LPArray (указатель на первый элемент массива в стиле C). Поскольку такой тип массива не дает информации о ранге и границах связанных данных массива, надо указать, какой параметр метода отвечает за предоставление информации о том, сколько элементов имеется в массиве. Это делается путем задания необязательного параметра SizeParamIndex, вдобавок к нему устанавливается необязательный параметр SizeConst, указывающий, что массив Param1 содержит еще 10 элементов, наряду с заданным аргументом ArraySize. Обратите внимание, что есть также тип массива SafeArray, являющийся самоописывающим массивом, содержащим тип, ранг и границы связанных данных, и не требующий задания дополнительных параметров в MarshalAsAttribute, но он специфичен для Microsoft и поэтому не описан здесь.

// Полный исходник: MarshalSpec\2.cs
// Двоичный файл: MarshalSpec\2.dll
// (...)

 public void TestMethod(
    [MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 2, SizeConst = 10)] int[] Param1,
    int ArraySize)
{
    // пустая команда
}

Следующая сигнатура MarshalSpec должна генерироваться вышеприведенным кодом.

Смещение

Значение

Что означает

0x1B

0x05

размер сигнатуры

0x1C

0x2A

тип параметра преобразования (ARRAY), смотрите таблицу констант для дескриптора преобразования.

0x1D

0x50

Константа MAX (смотрите таблицу констант для дескриптора преобразования) показывает, что этот массив не дает информации о типе элементов массива.

0x1E

0x02

Параметр ParamNum хранится как сжатое целое.

0x1F

0x0A

Параметр NumElem хранится как сжатое целое.

0x20

0x01

Параметр ElemMult хранится как сжатое целое. Это странный параметр, спецификация упоминает его лишь два раза, говоря, что если преобразуемый тип является ARRAY, то ElemMult должен быть установлен в 0x01, но не оговаривает его смысл и его местоположение в сигнатуре MarshalSpec (смотрите раздел §22.17 в спецификации метаданных часть II).