«Мечта лентяя» или скриптовый движок на самом себе +11
Эту давнюю мечту можно было бы назвать «мечтой лентяя», если бы имеющиеся общедоступные встраиваемые скриптовые средства были бы просты. Готовые средства существовали давно, например на платформе Windows, ещё в прошлом веке можно было использовать интерфейсы VBScript и Jscript через COM-интерфейс IActiveScriptSite. В настоящее время существует большое количество и других решений, например на базе Lua, но все они имеют одну неприятную особенность, сильно ограничивающую желание их применять.
Скрипты прекрасно работают и сами по себе, на них можно выполнять и логику, и арифметику, но пользы от них ровным счётом никакой, если сложно или нет возможности:
• добавлять функции и объекты для доступа к объектам разрабатываемой системы,
• проводить синтаксический контроль исходного скрипта и генерировать сообщения о синтаксических ошибках,
• выполнять скрипт в пошаговом режиме, подобно отладчику, с нотификацией точки исполнения и статусом.
И ещё, хотелось бы, чтобы делалось всё это просто и интуитивно понятно и не приходилось бы проводить бессонные ночи за чтением многочисленной документации по новому API. Увы, это удаётся далеко не всегда и весьма нечасто.
Прикладное ПО сейчас очень часто пишется на C#, и хотелось бы иметь что-то знакомое, но гибкое, и позволяющее писать скрипты. Такое решение есть, и оно заслуживает пристального внимания. Это пространство имён System.CodeDom.Compiler с его классом CSharpCodeProvider. Всё это появилось ещё в .NET 4.0, но по какой-то причине в большинстве публикаций по C# не затрагивался вопрос написания скриптов на C#, используя сам же язык C# в качестве базового. А это очень и очень удобно для написания и дальнейшего сопровождения продукта.
В этом случае самый главный и интересный метод — CompileAssemblyFromSource(), который выполняет компиляцию, генерирует сообщения об ошибках, и мы уже запросто можем написать «Hello world!»
using System;
using System.IO;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Reflection;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
// готовим текст скрипта
StringBuilder sb = new StringBuilder();
sb.AppendLine("using System;");
sb.AppendLine("namespace ConsoleApplication1");
sb.AppendLine("{");
sb.AppendLine(" public class MyScripter");
sb.AppendLine(" {");
sb.AppendLine(" public void Hello()");
sb.AppendLine(" {");
sb.AppendLine(" Console.WriteLine(\"Hello world!\");");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine("}");
// компилируем
CSharpCodeProvider codeProvider = new CSharpCodeProvider();
CompilerResults compileResults = codeProvider.CompileAssemblyFromSource(
new CompilerParameters(), new string[] { sb.ToString() });
// выводим ошибки, если они есть
foreach (CompilerError err in compileResults.Errors)
Console.WriteLine("Error({0:1}): {2} {3}", err.Line, err.Column,
err.ErrorNumber, err.ErrorText);
if (compileResults.Errors.HasErrors) return;
// загружаем получившуюся dll в память
byte[] dllBytes = File.ReadAllBytes(compileResults.PathToAssembly);
Assembly asmDll = Assembly.Load(dllBytes, null);
Type objType = asmDll.GetType("ConsoleApplication1.MyScripter");
// создаём объект класса из скрипта
object oClassInst = Activator.CreateInstance(objType);
// получаем точка входа и выполняем её
MethodInfo entry = objType.GetMethod("Hello", new Type[] {});
entry.Invoke(oClassInst, null);
}
}
}
Запускаем на исполнение:
![image](https://habrastorage.org/files/b20/cb2/9ff/b20cb29ffc474ce0b756a7acc47b9290.jpg)
Итак, в простейшем виде скрипт на C# успешно работает. Простым изменением текста скрипта мы можем влиять на его работу. Собственно, осталось лишь передать в скрипт в качестве примера какой-либо объект из основной программы.
В качестве такого объекта вполне подойдёт объект типа string:
using System;
using System.IO;
using Microsoft.CSharp;
using System.CodeDom.Compiler;
using System.Reflection;
using System.Text;
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
string sMyStr = "Before Script.";
// готовим текст скрипта
StringBuilder sb = new StringBuilder();
sb.AppendLine("using System;");
sb.AppendLine("namespace ConsoleApplication1");
sb.AppendLine("{");
sb.AppendLine(" public class MyScripter");
sb.AppendLine(" {");
sb.AppendLine(" public void Hello(ref string s)");
sb.AppendLine(" {");
sb.AppendLine(" Console.WriteLine(\"Hello world!\");");
sb.AppendLine(" s=\"After Script.\";");
sb.AppendLine(" }");
sb.AppendLine(" }");
sb.AppendLine("}");
// компилируем
CSharpCodeProvider codeProvider = new CSharpCodeProvider();
CompilerResults compileResults = codeProvider.CompileAssemblyFromSource(
new CompilerParameters(), new string[] { sb.ToString() });
// выводим ошибки, если они есть
foreach (CompilerError err in compileResults.Errors)
Console.WriteLine("Error({0:1}): {2} {3}", err.Line, err.Column,
err.ErrorNumber, err.ErrorText);
if (compileResults.Errors.HasErrors) return;
// загружаем получившуюся dll в память
byte[] dllBytes = File.ReadAllBytes(compileResults.PathToAssembly);
Assembly asmDll = Assembly.Load(dllBytes, null);
Type objType = asmDll.GetType("ConsoleApplication1.MyScripter");
// создаём объект класса из скрипта
object oClassInst = Activator.CreateInstance(objType);
// получаем точка входа и готовим параметры
MethodInfo entry = objType.GetMethod("Hello",
new Type[] { typeof(string).MakeByRefType() });
Object[] param = new Object[] { sMyStr };
Console.WriteLine(param[0]); // до выполнения скрипта
entry.Invoke(oClassInst, param); // вызов метода
Console.WriteLine(param[0]); // после выполнения скрипта
}
}
}
Запускаем на исполнение:
![image](https://habrastorage.org/files/a01/f95/128/a01f951289d44165ad07bd4c13c51f26.jpg)
Видно, что теперь мы можем передавать и возвращать значения из кода скрипта. Если в качестве параметра передать не ссылку на строку, а какой-то внутренний объект информационной системы, то мы вполне можем воздействовать на систему из скрипта.
У данного механизма есть режим исполнения в режиме отладчика, для этого нужно подключать .pdb файл, есть и много других интересных возможностей.
Недостатком подхода можно считать только то, что при компиляции создаётcя dll во временном каталоге ОС.
Путь разрешения этого недостатка ведёт нас в сторону использования пространства System.Reflection.Emit, но это достаточно объёмный материал, подходящий для отдельной статьи. Это сложно, т. к. в данном случае компилятор и генерацию придется писать самостоятельно. Но зато какие возможности по придумыванию своего собственного синтаксиса! Да и назвать новый язык программирования можно в честь себя или любимой кошки.
Удачи!
Аркадий Пчелинцев, архитектор проектов
Комментарии (9)
molnij
24.04.2017 10:44+8Ох сколько вам открытий чудных…
Во-первых, если вы будете исполнять скрипты чуть больше чем hello world, вдруг выяснится отличие интерпретируемых от компилируемых языков — фаза компиляции ни разу не бесплатная. И если у интерпретируемых она естественным образом встроена в воркфлоу, то у шарпа вполне можно огрести заметные тормоза на ней.
Потом обнаружится, что «гадить» в общий домен временными сборками скрипта — идея так себе, особенно если вспомнить, что удалить однажды загруженную сборку из домена невозможно (это слегка коррелирует с наличием временных файлов .dll, но проблема совсем в другом). А если делать по-науке и плодить домены, то придется изрядно позаниматься любовью с передачей нетривиальных параметров между доменами.
А статей на эту тему достаточно. Даже на хабре поиск по CSharpCodeProvider выдает вполне адекватную подборку.Sinatr
25.04.2017 10:46Хочется добавить, что решение проблемы (необходимость скриптовать некие сценарии) с помощью CodeDOM не совсем верно в общем случае (а случай в статье как раз общий, ни слова о конкретном сценарии).
Полезность примера выводящего «Hello world» в данном случае тоже близка с 0.
Единственно что обьяснили — это как скормить исходник и получить список ошибок, а это, извините, на статью не тянет, тема не раскрыта вообще от слова «полностью».
Я в свое время (эмм… десять лет назад) использовал CodeDOM для пре-компиляции формул (вызывался метод, тело которого пользователь писал на C#, методу передавался список входных/выходных параметров, которые можно было использовать в вычислениях и логике и возвращать результат), с появление Roslyn это конечно смешно (хотя уверен новичкам сгодилось), и тут эта статья…
kuda78
24.04.2017 11:20+1добавлять функции и объекты для доступа к объектам разрабатываемой системы
Необходимо реализовать у объекта интерфейс IDispatch. В C# с использованием рефлексии реализация доступа к методам и свойствам будет элементарен.
проводить синтаксический контроль исходного скрипта и генерировать сообщения о синтаксических ошибках
А чем этот момент не устроил в ActiveScriptSite? В ошибке будет указан и номер строки и что за ошибка.
выполнять скрипт в пошаговом режиме, подобно отладчику, с нотификацией точки исполнения и статусом.
С этим вопросом тоже все очень просто.
https://docs.microsoft.com/en-us/scripting/winscript/active-script-debugging-overview
mxms
24.04.2017 12:45В этом отношении идеальный вариант это web-сервер на Forth, конфигурируемый Forth и со скриптингом на Forth.
ColdPhoenix
простите, но вы, как минимум, на год опоздали.
Сейчас гораздо лучше брать Roslyn и интегрировать его.
EasyLy
Roslyn – хорошая штука, но требует .Net 4.5.2. Ниже он не соберётся. Да и великоват он
для простых задач.