Введение
Работая тут над одним проектом, потребовалось мне, что бы функциональность проекта расширялась на лету и сторонними разработчиками, причём возможностей к расширению было как можно больше, с возможностью правки кода на лету. Соответственно плагины для этого не очень годились из-за необходимости их постоянной перекомпиляции после любой правки. Выход: скрипты. До этого со скриптами я работал достаточно давно и это была Lua на C++. Вариант хороший, если бы не несколько минусов:
- Отсутствие нормальной реализации или прослойки под .NET/Mono — во всех что я видел были какие-то недоработки (может и плохо искал — как минимум пропустил Lua в TaoFramework)
- По всей видимости, нужно было писать кучу биндингов что бы среда исполнения .NET/Mono могла нормально взаимодействовать со средой Lua и обратно.
Тогда-то я и задался вопросом — а может быть в .NET/Mono уже есть что-нибудь для реализации скриптов? И ответ был да. Пространство имён "System.CodeDom.Compiler" было как раз то, что мне надо — возможность реализовать скрипты которые максимально соединялись со средой .NET/Mono.Правда если посмотреть на мой механизм скриптов изнутри, то это получается и не скрипты даже, а код, написанный на C#, просто динамически компилируется и загружается в память для выполнения. Однако, не смотря даже на такой «фейк», результата я добился — я мог править код, на лету перекомпилируя его прямо в своём приложении. И при этом это будет работать даже на машинах где не установлены Visual Studio и другие средства разработки, так как компиляторы, как минимум, C# и VB.NET идут прямо вместе с .NET и Mono.
Пример, о котором говориться в статье, можно скачать тут: http://zebraxxl.programist.ru/ScriptsInDotNet.zip
Пространство имён "System.CodeDom.Compiler"
Собственно самое главное в данной статье. Именно здесь собраны классы для компиляции кода в .NET/Mono сборки. Основным классом выполняющим работу является класс CodeDomProvider. По своей сути фактически — это интерфейс над компилятором, который просто подготавливает данные для компиляции и вызывает компилятор выбранного языка с нужными параметрами. Ну начнём по порядку.
А какие языки у нас вообще поддерживаются?
Что бы это выяснить достаточно вызвать статический метод CodeDomProvider.GetAllCompilerInfo. Данный метод вернёт массив СompilerInfo. В теории конечно может быть и так, что один компилятор поддерживает сразу несколько языков, но на практике я пока с таким не встречался. Но как раз на случай «многоязычности» компилятора в .NET/Mono как раз и сделано так, что сначала мы получаем информацию об отдельных компиляторах, а уже потом смотрим кто, какой язык поддерживает. Вот примеры вывода этой информации из примера:
0 compiler languages:
c#
cs
csharp
1 compiler languages:
vb
vbs
visualbasic
vbscript
2 compiler languages:
js
jscript
javascript
3 compiler languages:
vj#
vjs
vjsharp
4 compiler languages:
c++
mc
cpp
Как здесь может быть видно — фактически у меня в распоряжении имеется 5 языков: C#, VB.NET, J#, JScript, C++. Так же вывод показывает что каждым компилятором обслуживается один язык.
Здесь можно ещё добавить что на других компьютерах (без установленной Visual Studio — машина моего друга, который к IT не имеет никакого отношения и моём сервере Ubuntu server 11.04, Mono из репозитария (2.6.7)) выдаёт такой же результат.
А что нам вообще даёт поддержка множества языков? А даёт нам это то, что мы сможем писать скрипты на различных языках и при этом совершенно не задумываясь об их обработке их в приложении. А это в свою очередь повышает уровень вхождения сторонних разработчиков.
А теперь она! Компиляция
Ну а теперь попробуем скомпилировать что-нибудь. Напишем какой-нибудь тестовый класс на C#. Его можно найти в файле «TestScript.cs». Сам по себе класс достаточно простой — всего два метода. Один статический, другой нет. Оба просто выводят текст в консоль и возвращают строку. Так же в статический передаётся один параметр.
Для начала компиляции нам надо получить чем компилировать. Для этого выполняем статический метод "CodeDomProvider.CreateProvider". В качестве параметра он принимает название языка, на котором компилируем. Сами названия мы получали на предыдущем шаге.
Следующим шагом будем заполнять параметры компиляции. Все параметры хранятся в классе "CompilerParameters". Наиболее интересные поля:
- CompilerOptions — дополнительные параметры компилятору (т.е. что будет передваться, к примеру, csc в случае с C#)
- GenerateExecutable — генерировать сборку в исполняемом файле. Вообще по факту он в любом случае будет сделан. Только если это поле false — то исполняемый файл будет сгенерирован во временной папке. А если true — то он окажется там, где скажет поле "OutputAssembly"
- GenerateInMemory — генерировать сборку в памяти. Опять же по факту сначала сборка будет в исполняемом файле, откуда будет загружаться.
- IncludeDebugInformation — очень полезное поле. Если записать сюда true, то будет сгенерированная отладочная информация и можно будет отлаживать скрипты прямо параллельно с основным приложением.
- ReferencedAssemblies — какие сборки использовать при компиляции. Тоже нужное поле. В моём проекте я записывал сюда имя основного приложения что бы скрипты имели доступ его структуре и могли полноценно интегрироваться с приложением.
Ну а теперь все просто — взываем CodeDomProvider.CompileAssemblyFrom* в зависимости от того, откуда получаем исходник. Тут три варианта:
- CompileAssemblyFromFile — из файла. Последний параметр — список имён файлов для компиляции
- CompileAssemblyFromSource — из строк. Подразумевается что мы уже прочитали исходные файлы и они у нас хранятся в массиве строк в последнем параметре
- CompileAssemblyFromDom — из DOM дерева исходных кодов. Что-то типа DOM для html (или xml) документа
Первый параметр во всех этих методах — параметры "CompilerParameters". В качестве результата получаем "CompilerResults". Отсюда можно получить всю информацию о том, как прошла компиляция. Самые интересные и нужные поля "CompilerResults":
- CompiledAssembly — полученная сборка загруженная в память (если было установлено поле GenerateInMemory
- Errors — ошибки и предупреждения которые выдал компилятор
Итак если CompiledAssembly не null (то есть сборка скомпилировалась успешно) то имеется мы уже можем пользоваться нашим скриптом как полноценной сборкой .NET/Mono получая доступ к необходимым нам функциям и полям через рефлексию. Я не буду останавливаться на этой части более подробно — всё таки это уже другая тема.
А скрипты попроще?
Это конечно хорошо использование такого мощного языка в качестве скриптов — это открывает огромные возможности для расширения функционала приложения, но в некоторых случаях это может оказаться забиванием гвоздей микроскопом. Разница в двух вариантах сложности кода очевидна:
using System;
namespace Script
{
public class Script
{
public static void M()
{
Console.WriteLine(“Hello world”);
}
}
}
Или такой вариант:
WriteLine(“Hello World”);
Два кода которые в принципе делают одно и то же (при условии что во втором варианте под WriteLine понимается вызов System.Console.WriteLine) — но как говориться: «Почувствуйте разницу».
Способов добиться такого упрощения можно придумать множество. И у каждого из них будут свои преимущества и недостатки. Однако все они будут сводиться к одному: к динамической генерации «большого» кода из «маленького». Для примера я взял самый простой способ, который смог придумать:
- Создал отдельный класс (далее класс-реализация) который содержит в себе все методы, готорые в скриптах являются глобальными (в примере — класс MiniScriptWorker).
- Создал шаблон для генерации «большого» кода — один дочерний от класса-реализации класс с одним пустым методом (файл «ScriptTemplate.cs»)
- «Маленький» код просто вставляется в реализацию того самого пустого метода класса-шаблона (файл «MiniScript.cs»)
Заключение
Вот таким несложным путём мы получаем достаточно мощные скрипты практически бесплатно в .NET/Mono приложении без необходимости тянуть за собой дополнительные зависимости. Да — в некоторых случаях такой способ наверно будет даже избыточен, но всё зависит всё-таки от задачи — в моём случае именно такая реализация была самой лучшей и удобной.
Ещё раз о плюсах такого способа: в качестве скриптов у вас будет мощный язык с практически полным доступом к среде .NET/Mono (насколько полный решать вам — какие сборки будете добавлять во время компиляции, то и будет доступно скриптам). Так же, просто добавив своё приложение в список сборок для скрипта, вы получите практически полную интеграцию с вашим приложением и возможность видоизменять его так, как вам, или разработчику скрипта, захочется.
Однако из этого растут и минусы. Иногда может потребоваться скрыть какой-то кусоск основного приложения от скрипта. От этого можно защитится путём сокрытия таких частей в область видимости "internal". Правда эту защиту можно обойти при помощи другой проблемы — из-за такого широкого доступа к среде .NET/Mono страдает безопасность. К примеру нам ничего не сможет помешать используя System.IO.FileStream открыть какой-нибудь файл с паролями и потом спокойно отослать его содержимое куда-нибудь в чужие руки. К сожалению решения данной проблемы у меня пока нет — не успел разобрать вопрос более подробно.