Как стать автором
Обновить

Скрипты в .NET/Mono средствами самой платформы

Время на прочтение6 мин
Количество просмотров4.9K

Введение


Работая тут над одним проектом, потребовалось мне, что бы функциональность проекта расширялась на лету и сторонними разработчиками, причём возможностей к расширению было как можно больше, с возможностью правки кода на лету. Соответственно плагины для этого не очень годились из-за необходимости их постоянной перекомпиляции после любой правки. Выход: скрипты. До этого со скриптами я работал достаточно давно и это была 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) — но как говориться: «Почувствуйте разницу».
Способов добиться такого упрощения можно придумать множество. И у каждого из них будут свои преимущества и недостатки. Однако все они будут сводиться к одному: к динамической генерации «большого» кода из «маленького». Для примера я взял самый простой способ, который смог придумать:
  1. Создал отдельный класс (далее класс-реализация) который содержит в себе все методы, готорые в скриптах являются глобальными (в примере — класс MiniScriptWorker).
  2. Создал шаблон для генерации «большого» кода — один дочерний от класса-реализации класс с одним пустым методом (файл «ScriptTemplate.cs»)
  3. «Маленький» код просто вставляется в реализацию того самого пустого метода класса-шаблона (файл «MiniScript.cs»)


Заключение


Вот таким несложным путём мы получаем достаточно мощные скрипты практически бесплатно в .NET/Mono приложении без необходимости тянуть за собой дополнительные зависимости. Да — в некоторых случаях такой способ наверно будет даже избыточен, но всё зависит всё-таки от задачи — в моём случае именно такая реализация была самой лучшей и удобной.
Ещё раз о плюсах такого способа: в качестве скриптов у вас будет мощный язык с практически полным доступом к среде .NET/Mono (насколько полный решать вам — какие сборки будете добавлять во время компиляции, то и будет доступно скриптам). Так же, просто добавив своё приложение в список сборок для скрипта, вы получите практически полную интеграцию с вашим приложением и возможность видоизменять его так, как вам, или разработчику скрипта, захочется.
Однако из этого растут и минусы. Иногда может потребоваться скрыть какой-то кусоск основного приложения от скрипта. От этого можно защитится путём сокрытия таких частей в область видимости "internal". Правда эту защиту можно обойти при помощи другой проблемы — из-за такого широкого доступа к среде .NET/Mono страдает безопасность. К примеру нам ничего не сможет помешать используя System.IO.FileStream открыть какой-нибудь файл с паролями и потом спокойно отослать его содержимое куда-нибудь в чужие руки. К сожалению решения данной проблемы у меня пока нет — не успел разобрать вопрос более подробно.
Теги:
Хабы:
+27
Комментарии35

Публикации

Изменить настройки темы

Истории

Работа

.NET разработчик
74 вакансии

Ближайшие события

Weekend Offer в AliExpress
Дата20 – 21 апреля
Время10:00 – 20:00
Место
Онлайн
Конференция «Я.Железо»
Дата18 мая
Время14:00 – 23:59
Место
МоскваОнлайн