У разработчиков прикладного ПО очень часто возникает потребность встроить в свой продукт некий скриптовый язык, который бы решал часть задач, не описанных детально на момент проектирования системы. Действительно удобно: и возможность расширения функциональности есть, и трудоёмкость создания такого решения, на первый взгляд, невелика.
Эту давнюю мечту можно был�� бы назвать «мечтой лентяя», если бы имеющиеся общедоступные встраиваемые скриптовые средства были бы просты. Готовые средства существовали давно, например на платформе Windows, ещё в прошлом веке можно было использовать интерфейсы VBScript и Jscript через COM-интерфейс IActiveScriptSite. В настоящее время существует большое количество и других решений, например на базе Lua, но все они имеют одну неприятную особенность, сильно ограничивающую желание их применять.
Скрипты прекрасно работают и сами по себе, на них можно выполнять и логику, и арифметику, но пользы от них ровным счётом никакой, если сложно или нет возможности:
• добавлять функции и объекты для доступа к объектам разрабатываемой системы,
• проводить синтаксический контроль исходного скрипта и генерировать сообщения о синтаксических ошибках,
• выполнять скрипт в пошаговом режиме, подобно отладчику, с нотификацией точки исполнения и статусом.
И ещё, хотелось бы, чтобы делалось всё это просто и интуитивно понятно и не приходилось бы проводить бессонные ночи за чтением многочисленной документации по новому API. Увы, это удаётся далеко не всегда и весьма нечасто.
Прикладное ПО сейчас очень часто пишется на C#, и хотелось бы иметь что-то знакомое, но гибкое, и позволяющее писать скрипты. Такое решение есть, и оно заслуживает пристального внимания. Это пространство имён System.CodeDom.Compiler с его классом CSharpCodeProvider. Всё это появилось ещё в .NET 4.0, но по какой-то причине в большинстве публикаций по C# не затрагивался вопрос написания скриптов на C#, используя сам же язык C# в качестве базового. А это очень и очень удобно для написания и дальнейшего сопровождения продукта.
В этом случае самый главный и интересный метод — CompileAssemblyFromSource(), который выполняет компиляцию, генерирует сообщения об ошибках, и мы уже запросто можем написать «Hello world!»
Запускаем на исполнение:

Итак, в простейшем виде скрипт на C# успешно работает. Простым изменением текста скрипта мы можем влиять на его работу. Собственно, осталось лишь передать в скрипт в качестве примера какой-либо объект из основной программы.
В качестве такого объекта вполне подойдёт объект типа string:
Запускаем на исполнение:

Видно, что теперь мы можем передавать и возвращать значения из кода скрипта. Если в качестве параметра передать не ссылку на строку, а какой-то внутренний объект информационной системы, то мы вполне можем воздействовать на систему из скрипта.
У данного механизма есть режим исполнения в режиме отладчика, для этого нужно подключать .pdb файл, есть и много других интересных возможностей.
Недостатком подхода можно считать только то, что при компиляции создаётcя dll во временном каталоге ОС.
Путь разрешения этого недостатка ведёт нас в сторону использования пространства System.Reflection.Emit, но это достаточно объёмный материал, подходящий для отдельной статьи. Это сложно, т. к. в данном случае компилятор и генерацию придется писать самостоятельно. Но зато какие возможности по придумыванию своего собственного синтаксиса! Да и назвать новый язык программирования можно в честь себя или любимой кошки.
Удачи!
Аркадий Пчелинцев, архитектор проектов
Эту давнюю мечту можно был�� бы назвать «мечтой лентяя», если бы имеющиеся общедоступные встраиваемые скриптовые средства были бы просты. Готовые средства существовали давно, например на платформе 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);
}
}
}
Запускаем на исполнение:

Итак, в простейшем виде скрипт на 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]); // после выполнения скрипта
}
}
}
Запускаем на исполнение:

Видно, что теперь мы можем передавать и возвращать значения из кода скрипта. Если в качестве параметра передать не ссылку на строку, а какой-то внутренний объект информационной системы, то мы вполне можем воздействовать на систему из скрипта.
У данного механизма есть режим исполнения в режиме отладчика, для этого нужно подключать .pdb файл, есть и много других интересных возможностей.
Недостатком подхода можно считать только то, что при компиляции создаётcя dll во временном каталоге ОС.
Путь разрешения этого недостатка ведёт нас в сторону использования пространства System.Reflection.Emit, но это достаточно объёмный материал, подходящий для отдельной статьи. Это сложно, т. к. в данном случае компилятор и генерацию придется писать самостоятельно. Но зато какие возможности по придумыванию своего собственного синтаксиса! Да и назвать новый язык программирования можно в честь себя или любимой кошки.
Удачи!
Аркадий Пчелинцев, архитектор проектов
