Язык программирования C#9 и платформа .NET5 - Эндрю Троелсен
Шрифт:
Интервал:
Закладка:
Заталкивание и выталкивание: основанная на стеке природа CIL
В языках .NET Core высокого уровня (таких как С#) предпринимается попытка насколько возможно скрыть из виду низкоуровневые детали CIL. Один из особенно хорошо скрываемых аспектов — тот факт, что CIL является языком программирования, основанным на использовании стека. Вспомните из исследования пространств имен коллекций (см. главу 10), что класс Stack<T> может применяться для помещения значения в стек, а также для извлечения самого верхнего значения из стека с целью последующего использования. Разумеется, программисты на языке CIL не работают с объектом типа Stack<T> для загрузки и выгрузки вычисляемых значений, но применяемый ими образ действий похож на заталкивание и выталкивание.
Формально сущность, используемая для хранения набора вычисляемых значений, называется виртуальным стеком выполнения. Вы увидите, что CIL предоставляет несколько кодов операций, которые служат для помещения значения в стек; такой процесс именуется загрузкой. Кроме того, в CIL определены дополнительные коды операций, которые перемещают самое верхнее значение из стека в память (скажем, в локальную переменную), применяя процесс под названием сохранение.
В мире CIL невозможно напрямую получать доступ к элементам данных, включая локально определенные переменные, входные аргументы методов и данные полей типа. Вместо этого элемент данных должен быть явно загружен в стек и затем извлекаться оттуда для использования в более позднее время (запомните упомянутое требование, поскольку оно содействует пониманию того, почему блок кода CIL может выглядеть несколько избыточным).
На заметку! Вспомните, что код CIL не выполняется напрямую, а компилируется по требованию. Во время компиляции кода CIL многие избыточные аспекты реализации оптимизируются. Более того, если для текущего проекта включена оптимизация кода (на вкладке Build (Сборка) окна свойств проекта в Visual Studio), то компилятор будет также удалять разнообразные избыточные детали CIL.
Чтобы понять, каким образом CIL задействует модель обработки на основе стека, создайте простой метод C# по имени PrintMessage(), который не принимает аргументов и возвращает void. Внутри его реализации будет просто выводиться значение локальной переменной в стандартный выходной поток:
void PrintMessage()
{
string myMessage = "Hello.";
Console.WriteLine(myMessage);
}
Если просмотреть код CIL, который получился в результате трансляции метода PrintMessage() компилятором С#, то первым делом обнаружится, что в нем определяется ячейка памяти для локальной переменной с помощью директивы .locals. Затем локальная строка загружается и сохраняется в этой локальной переменной с применением кодов операций ldstr (загрузить строку) и stloc.0 (сохранить текущее значение в локальной переменной, находящейся в ячейке 0).
Далее с помощью кода операции ldloc.0 (загрузить локальный аргумент по индексу 0) значение (по индексу 0) загружается в память для использования в вызове метода System.Console.WriteLine(), представленном кодом операции call. Наконец, посредством кода операции ret производится возвращение из функции. Ниже показан (прокомментированный) код CIL для метода PrintMessage() (ради краткости из листинга были удалены коды операций nop):
.method assembly hidebysig static void PrintMessage() cil managed
{
.maxstack 1
// Определить локальную переменную типа string (по индексу 0).
.locals init ([0] string V_0)
// Загрузить в стек строку со значением "Hello."
ldstr " Hello."
// Сохранить строковое значение из стека в локальной переменной.
stloc.0
// Загрузить значение по индексу 0.
ldloc.0
// Вызвать метод с текущим значением.
call void [System.Console]System.Console::WriteLine(string)
ret
}
На заметку! Как видите, язык CIL поддерживает синтаксис комментариев в виде двойной косой черты (и вдобавок синтаксис /*...*/). Подобно компилятору C# компилятор CIL игнорирует комментарии в коде.
Теперь, когда вы знаете основы директив, атрибутов и кодов операций CIL, давайте приступим к практическому программированию на CIL, начав с рассмотрения темы возвратного проектирования.
Возвратное проектирование
В главе 1 было показано, как применять утилиту ildasm.exe для просмотра кода CIL, сгенерированного компилятором С#. Тем не менее, вы можете даже не подозревать, что эта утилита позволяет сбрасывать код CIL, содержащийся внутри загруженной в нее сборки, во внешний файл. Полученный подобным образом код CIL можно редактировать и компилировать заново с помощью компилятора CIL (ilasm.exe).
Выражаясь формально, такой прием называется возвратным проектированием и может быть полезен в избранных обстоятельствах, которые перечислены ниже.
• Вам необходимо модифицировать сборку, исходный код которой больше не доступен.
• Вы работаете с далеким от идеала компилятором языка .NET Core, который генерирует неэффективный (или явно некорректный) код CIL, поэтому нужно изменять кодовую базу.
• Вы конструируете библиотеку взаимодействия с СОМ и хотите учесть ряд атрибутов COM IDL, которые были утрачены во время процесса преобразования (такие как COM-атрибут [helpstring]).
Чтобы ознакомиться с процессом возвратного проектирования, создайте новый проект консольного приложения .NET Core на языке C# по имени RoundTrip посредством интерфейса командной строки .NET Core (CLI):
dotnet new console -lang c# -n RoundTrip -o .RoundTrip -f net5.0
Модифицируйте операторы верхнего уровня, как показано ниже:
// Простое консольное приложение С#.
Console.WriteLine("Hello CIL code!");
Console.ReadLine();
Скомпилируйте программу с применением интерфейса CLI:
dotnet build
На заметку! Вспомните из главы 1, что результатом компиляции всех сборок .NET Core (библиотек классов и консольных приложений) будут файлы с расширением *.dll, которые выполняются с применением интерфейса .NET Core CLI. Нововведением .NET Core 3+ (и последующих версий) является то, что файл dotnet.exe копируется в выходной каталог и переименовывается согласно имени сборки. Таким образом, хотя выглядит так, что ваш проект был скомпилирован в RoundTrip.exe, на самом деле он компилируется в RoundTrip.dll, а файл dotnet.exe копируется в RoundTrip.exe вместе с обязательными аргументами командной строки, необходимыми для запуска Roundtrip.dll.
Запустите ildasm.exe в отношении RoundTrip.dll, используя следующую команду (на уровне каталога решения):