Язык программирования C#9 и платформа .NET5 - Эндрю Троелсен
Шрифт:
Интервал:
Закладка:
}
то входные аргументы а и b загружались бы с применением кодов операций ldarg.1 и ldarg.2 (а не ожидаемых ldarg.0 и ldarg.1). Причина в том, что ячейка 0 содержит неявную ссылку this. Взгляните на следующий псевдокод:
// Это ТОЛЬКО псевдокод!
.method public hidebysig static int32 AddTwoIntParams(
MyClass_HiddenThisPointer this, int32 a, int32 b) cil managed
{
ldarg.0 // Load MyClass_HiddenThisPointer onto the stack.
ldarg.1 // Load "a" onto the stack.
ldarg.2 // Load "b" onto the stack.
...
}
Представление итерационных конструкций в CIL
Итерационные конструкции в языке программирования C# реализуются посредством ключевых слов for, foreach, while и do, каждое из которых имеет специальное представление в CIL. В качестве примера рассмотрим следующий классический цикл
for:
public static void CountToTen()
{
for(int i = 0; i < 10; i++)
{
}
}
Вспомните, что для управления прекращением потока выполнения, когда удовлетворено некоторое условие, используются коды операций br (br, bltn т.д.). В приведенном примере указано условие, согласно которому выполнение цикла for должно прекращаться, когда значение локальной переменной i становится больше или равно 10. С каждым проходом к значению i добавляется 1, после чего проверяемое условие оценивается заново.
Также вспомните, что в случае применения любого кода операции CIL, предназначенного для ветвления, должна быть определена специфичная метка кода (или две), обозначающая место, куда будет произведен переход при истинном результате оценки условия. С учетом всего сказанного рассмотрим показанный ниже (отредактированный) код CIL, который сгенерирован утилитой ildasm.exe (вместе с автоматически созданными метками):
.method public hidebysig static void CountToTen() cil managed
{
.maxstack 2
.locals init (int32 V_0, bool V_1)
IL_0000: ldc.i4.0 // Загрузить это значение в стек.
IL_0001: stloc.0 // Сохранить это значение по индексу 0.
IL_0002: br.s IL_000b // Перейти на метку IL_ 0008.
IL_0003: ldloc.0 // Загрузить значение переменной по индексу 0.
IL_0004: ldc.i4.1 // Загрузить значение 1 в стек.
IL_0005: add // Добавить текущее значение в стеке по индексу 0.
IL_0006: stloc.0
IL_0007: ldloc.0 // Загрузить значение по индексу 0.
IL_0008: ldc.i4.s 10 // Загрузить значение 10 в стек.
IL_0009: clt // Меньше значения в стеке?
IL_000a: stloc.1 // Сохранить результат по индексу 1.
IL_000b: ldloc.1 // Загрузить значение переменной по индексу 1.
IL_000c: brtrue.s IL_0002 // Если истинно, тогда перейти на метку IL 0002.
IL_000d: ret
}
Код CIL начинается с определения локальной переменной типа int32 и ее загрузки в стек. Затем производятся переходы туда и обратно между метками IL_0008 и IL_0004, во время каждого из которых значение i увеличивается на 1 и проверяется на предмет того, что оно все еще меньше 10. Как только условие будет нарушено, осуществляется выход из метода.
Заключительные слова о языке CIL
Ознакомившись с процессом создания исполняемого файла из файла *.il, вероятно у вас возникла мысль о том, что он требует чрезвычайно много работы и затем вопрос, в чем здесь выгода. В подавляющем большинстве случаев вы никогда не будете создавать исполняемый файл .NET Core из файла *.il. Тем не менее, способность понимать код CIL может принести пользу, когда вам нужно исследовать сборку, для которой отсутствует исходный код.
Существуют также коммерческие инструменты, которые восстанавливают исходный код сборки .NET Core. Если вам доводилось когда-либо пользоваться одним из инструментов подобного рода, то теперь вы знаете, каким образом они работают!
Динамические сборки
Естественно, процесс построения сложных приложений .NET Core на языке CIL будет довольно-таки неблагодарным трудом. С одной стороны, CIL является чрезвычайно выразительным языком программирования, который позволяет взаимодействовать со всеми программными конструкциями, разрешенными CTS. С другой стороны, написание низкоуровневого кода CIL утомительно, сопряжено с большими затратами времени и подвержено ошибкам. Хотя и правда, что знание — сила, вас может интересовать, насколько важно держать в уме все правила синтаксиса CIL. Ответ: зависит от ситуации. Разумеется, в большинстве случаев при программировании приложений .NET Core просматривать, редактировать или писать код CIL не потребуется. Однако знание основ языка CIL означает готовность перейти к исследованию мира динамических сборок (как противоположности статическим сборкам) и роли пространства имен System.Reflection.Emit.
Первым может возникнуть вопрос: чем отличаются статические сборки от динамических? По определению статической сборкой называется двоичная сборка .NET, которая загружается прямо из дискового хранилища, т.е. на момент запроса средой CLR она находится где-то на жестком диске в физическом файле (или в наборе файлов, если сборка многофайловая). Как и можно было предположить, при каждой компиляции исходного кода C# в результате получается статическая сборка.
Что касается динамической сборки, то она создается в памяти на лету с использованием типов из пространства имен System.Reflection.Emit, которое делает возможным построение сборки и ее модулей, определений типов и логики реализации на языке CIL во время выполнения. Затем сборку, расположенную в памяти, можно сохранить на диск, получив в результате новую статическую сборку. Ясно, что процесс создания динамических сборок с помощью пространства имен System.Reflection.Emit требует понимания природы кодов операций CIL.
Несмотря на то что создание динамических сборок является сложной (и редкой) задачей программирования, оно может быть удобным в разнообразных обстоятельствах. Ниже перечислены примеры.
• Вы строите инструмент программирования .NET Core, который должен быть способным генерировать сборки по требованию на основе пользовательского ввода.
• Вы создаете приложение, которое нуждается в генерации посредников для удаленных типов на лету, основываясь на полученных метаданных.
• Вам необходима возможность загрузки статической сборки и динамической вставки в двоичный образ новых типов.
Давайте посмотрим, какие типы доступны в пространстве имен System.Reflection.Emit.
Исследование пространства имен System.Reflection.Emit
Создание динамической сборки требует некоторых знаний кодов операций CIL, но типы из пространства имен System.Reflection.Emit максимально возможно скрывают сложность языка CIL. Скажем, вместо указания необходимых директив и атрибутов CIL для определения