«Мечта лентяя» или скриптовый движок на самом себе

в 7:16, , рубрики: .net, C#

У разработчиков прикладного ПО очень часто возникает потребность встроить в свой продукт некий скриптовый язык, который бы решал часть задач, не описанных детально на момент проектирования системы. Действительно удобно: и возможность расширения функциональности есть, и трудоёмкость создания такого решения, на первый взгляд, невелика.

Эту давнюю мечту можно было бы назвать «мечтой лентяя», если бы имеющиеся общедоступные встраиваемые скриптовые средства были бы просты. Готовые средства существовали давно, например на платформе 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
Итак, в простейшем виде скрипт на 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

Видно, что теперь мы можем передавать и возвращать значения из кода скрипта. Если в качестве параметра передать не ссылку на строку, а какой-то внутренний объект информационной системы, то мы вполне можем воздействовать на систему из скрипта.

У данного механизма есть режим исполнения в режиме отладчика, для этого нужно подключать .pdb файл, есть и много других интересных возможностей.

Недостатком подхода можно считать только то, что при компиляции создаётcя dll во временном каталоге ОС.

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

Аркадий Пчелинцев, архитектор проектов

Автор: Павел Локтев

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js