Одной из самых роскошных тем для программистов, балующихся изобретением велосипедов, является написание собственных языков, интерпретаторов и компиляторов. Действительно, программа, способная создавать или исполнять другие программы, инстинктивно вселяет благоговейный трепет в сердца кодеров — потому что сложно, объемно, но безумно увлекательно.
Большинство начинают с собственных интерпретаторов, которые представляют собой в общем виде огромный свитч команд в цикле. Интересно, вольготно, но муторно и весьма медленно. Хочется чего-то более шустрого, чтобы JIT'ить умело и, желательно, само следило за памятью.
Отличным решением данной проблемы является выбор .NET в качестве целевой платформы. Оставим лексический разбор на следующий раз, а сегодня давайте попробуем сделать простейшую программу, которая создает работающий исполняемый экзешник:

Программа будет требовать имя, и выводить в консоли Hello, %username%.
Для создания экзешника существует много способов, например:
Как раз последний вариант я и выбрал. К сожалению, я не знаю, в чем для данной задачи Cecil превосходит Reflection, но мне попался пример именно на Cecil, поэтому именно его я и разберу.
Mono.Cecil — это библиотека, позволяющая работать со сборкой как с массивом байтов. C ее помощью можно как создавать свои собственные сборки, так и ковыряться и модифицировать уже существующие. Она предоставляет широкий спектр классов, которыми (обычно) удобно пользоваться.
Вот, собственно, готовый код (без описания класса, формы и всего прочего, кроме собственно метода-генератора):
Теперь более внимательно о жутко выглядящей центральной части, которая, собственно, генерирует код.
Написанная на С#, та же самая программа выглядела бы вот так (описание класса я опущу):
Для этого мы берем две строки, первая — константа, вторая определяется на этапе компиляции и тоже становится константой, кладем их в стек. String.Concat складывает эти строки и оставляет на вершине стека результат, который берется Console.WriteLine и выводится на экран.
После этого, чтобы программа не закрылась прежде, чем мы успеем что-то прочитать, требуем Console.ReadLine() — а поскольку оно возвращает считанную строку, которая нам не нужна, выкидываем ее из стека, после чего с чувством выполненного долга покидаем уже ставшую почти родной функцию Main.
Мы генерируем программу под виртуальную машину .NET, и тело метода состоит, очевидно, из ее команд. .NET — стековая виртуальная машина, поэтому все операции производятся с операндами, лежащими на стеке. Полный их список можно найти в википедии, а я же расскажу только о тех, которые использовал, более подробно.
LDSTR загружает в стек строку. Очевидно, в качестве параметра ему требуется строка. По сути, «загружает строку в стек» означает, что не сама строка в стек кладется, а только указатель на то место в памяти, где она располагается — но для нас, как для программистом IL, это не важно. Важно только то, что следующие команды смогут ее оттуда взять и использовать.
CALL, как можно догадаться из названия, вызывает метод. Для этого ему необходимо передать ссылку на объект с описанием этого самого метода, который предварительно нужно импортировать. Для импорта следует «найти» метод в типе, передав имя и список типов его параметров в виде массива — вот почему запись такая ужасная. По-хорошему, тут нужно было бы написать какой-нибудь обработчик, который преобразует строку вида «String.Concat(string, string)» вот в этот ужас — можете попробовать этим заняться.
POP выкидывает из стека верхний элемент. Ничего особенного. Нужен нам потому, что Console.ReadLine() возвращает значение, а наша функция — void, следовательно мы не можем там его оставить и должны очистить.
RET — от слова return, производит выход из текущей функции. Обязательно должна быть в конце каждой функции, а может быть и не одна — в зависимости от того, сколько у вас точек выхода из нее.
В конце концов, скомпилировав и запустив программу, введя туда свое имя и нажав увесистый кнопарь Compile, мы получаем в той же папке миниатюрный бинарничек greeter.exe, который весит ровно 2048 байт.
Запускаем его, и вуаля!

Большинство начинают с собственных интерпретаторов, которые представляют собой в общем виде огромный свитч команд в цикле. Интересно, вольготно, но муторно и весьма медленно. Хочется чего-то более шустрого, чтобы JIT'ить умело и, желательно, само следило за памятью.
Отличным решением данной проблемы является выбор .NET в качестве целевой платформы. Оставим лексический разбор на следующий раз, а сегодня давайте попробуем сделать простейшую программу, которая создает работающий исполняемый экзешник:

Программа будет требовать имя, и выводить в консоли Hello, %username%.
Для создания экзешника существует много способов, например:
- Трансляция в C#-код и вызов csc.exe: просто, но неспортивно
- Генерация IL-кода в текстовой форме и компиляция ilasm.exe: неудобно по причине необходимости писать руками огромный манифест
- Генерация сборки напрямую с помощью Reflection или Cecil
Как раз последний вариант я и выбрал. К сожалению, я не знаю, в чем для данной задачи Cecil превосходит Reflection, но мне попался пример именно на Cecil, поэтому именно его я и разберу.
Mono.Cecil — это библиотека, позволяющая работать со сборкой как с массивом байтов. C ее помощью можно как создавать свои собственные сборки, так и ковыряться и модифицировать уже существующие. Она предоставляет широкий спектр классов, которыми (обычно) удобно пользоваться.
Предмет беседы
Вот, собственно, готовый код (без описания класса, формы и всего прочего, кроме собственно метода-генератора):
using Mono.Cecil;
using Mono.Cecil.Cil;
public void Compile(string str)
{
// создаем библиотеку и задаем ее название, версию и тип: консольное приложение
var name = new AssemblyNameDefinition("SuperGreeterBinary", new Version(1, 0, 0, 0));
var asm = AssemblyDefinition.CreateAssembly(name, "greeter.exe", ModuleKind.Console);
// импортируем в библиотеку типы string и void
asm.MainModule.Import(typeof(String));
var void_import = asm.MainModule.Import(typeof(void));
// создаем метод Main, статический, приватный, возвращающий void
var method = new MethodDefinition("Main", MethodAttributes.Static | MethodAttributes.Private | MethodAttributes.HideBySig, void_import);
// сохраняем короткую ссылку на генератор кода
var ip = method.Body.GetILProcessor();
// магия ленор!
ip.Emit(OpCodes.Ldstr, "Hello, ");
ip.Emit(OpCodes.Ldstr, str);
ip.Emit(OpCodes.Call, asm.MainModule.Import(typeof(String).GetMethod("Concat", new Type[] { typeof(string), typeof(string) })));
ip.Emit(OpCodes.Call, asm.MainModule.Import(typeof(Console).GetMethod("WriteLine", new Type[] { typeof(string) })));
ip.Emit(OpCodes.Call, asm.MainModule.Import(typeof(Console).GetMethod("ReadLine", new Type[] { })));
ip.Emit(OpCodes.Pop);
ip.Emit(OpCodes.Ret);
// регистрируем тип, к которому будет привязан данный метод: все параметры выбраны
// опытным путем из дизассемблированного экзешника
var type = new TypeDefinition("supergreeter", "Program", TypeAttributes.AutoClass | TypeAttributes.Public | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit, asm.MainModule.Import(typeof(object)));
// добавляем тип в сборку
asm.MainModule.Types.Add(type);
// привязываем метод к типу
type.Methods.Add(method);
// указываем точку входа для исполняемого файла
asm.EntryPoint = method;
// сохраняем сборку на диск
asm.Write("greeter.exe");
}
Теперь более внимательно о жутко выглядящей центральной части, которая, собственно, генерирует код.
Что же там творится?
Написанная на С#, та же самая программа выглядела бы вот так (описание класса я опущу):
static public void Main()
{
Console.WriteLine("Hello, " + "username");
Console.ReadLine();
}
Для этого мы берем две строки, первая — константа, вторая определяется на этапе компиляции и тоже становится константой, кладем их в стек. String.Concat складывает эти строки и оставляет на вершине стека результат, который берется Console.WriteLine и выводится на экран.
После этого, чтобы программа не закрылась прежде, чем мы успеем что-то прочитать, требуем Console.ReadLine() — а поскольку оно возвращает считанную строку, которая нам не нужна, выкидываем ее из стека, после чего с чувством выполненного долга покидаем уже ставшую почти родной функцию Main.
О байткоде
Мы генерируем программу под виртуальную машину .NET, и тело метода состоит, очевидно, из ее команд. .NET — стековая виртуальная машина, поэтому все операции производятся с операндами, лежащими на стеке. Полный их список можно найти в википедии, а я же расскажу только о тех, которые использовал, более подробно.
LDSTR загружает в стек строку. Очевидно, в качестве параметра ему требуется строка. По сути, «загружает строку в стек» означает, что не сама строка в стек кладется, а только указатель на то место в памяти, где она располагается — но для нас, как для программистом IL, это не важно. Важно только то, что следующие команды смогут ее оттуда взять и использовать.
CALL, как можно догадаться из названия, вызывает метод. Для этого ему необходимо передать ссылку на объект с описанием этого самого метода, который предварительно нужно импортировать. Для импорта следует «найти» метод в типе, передав имя и список типов его параметров в виде массива — вот почему запись такая ужасная. По-хорошему, тут нужно было бы написать какой-нибудь обработчик, который преобразует строку вида «String.Concat(string, string)» вот в этот ужас — можете попробовать этим заняться.
POP выкидывает из стека верхний элемент. Ничего особенного. Нужен нам потому, что Console.ReadLine() возвращает значение, а наша функция — void, следовательно мы не можем там его оставить и должны очистить.
RET — от слова return, производит выход из текущей функции. Обязательно должна быть в конце каждой функции, а может быть и не одна — в зависимости от того, сколько у вас точек выхода из нее.
Результаты работы
В конце концов, скомпилировав и запустив программу, введя туда свое имя и нажав увесистый кнопарь Compile, мы получаем в той же папке миниатюрный бинарничек greeter.exe, который весит ровно 2048 байт.
Запускаем его, и вуаля!
