Скриптинг в C# или динамическое выполнение в runtime

от автора

Привет, Хабр!

Думаю, не многие знают, что в C# есть штука на подобии eval из других языков. Благодаря Roslyn API, можно во время выполнения скомпилировать и выполнить C# код. Пример использования можете посмотреть в моей реализации REPL-а для C#.

Впервые с такой штукой, как REPL, я познакомился когда теребил питона. В мире .NET есть похожая вещь под названием C# Interactive (CSI). Довольно удобная штука, но у нее есть один большой минус — она входит в состав инструментов Visual Studio, так что без установки VS, не получится ее использовать, а чтобы запускать ее без запуска VS, вообще надо лезть в ее недра (а точнее, через консоль запустить C:\Program Files (x86)\Microsoft Visual Studio\2019\Community\Common7\Tools\VsDevCmd.bat), что так себе.

Есть еще такие проекты, как dotnet-script и cs-script (они работают через Microsoft.CodeAnalysis.CSharp.Scripting), но у них есть фатальный недостаток — они написаны не мной. Вот и появилась мысль, написать свой корявый велосипед, но со своими фичами! (которые тоже коряво работают). После недолгих поисков, мой взор упал на сие чудо: Microsoft.CodeAnalysis.CSharp.Scripting. Из плюсов — удобный API, возможность выполнять код без классов и namespace-ов.

Для начала, нужно поставить нугет пакет Microsoft.CodeAnalysis.CSharp.Scripting и сделать using

using Microsoft.CodeAnalysis.CSharp.Scripting; using Microsoft.CodeAnalysis.Scripting;

CSharpScript — статичный класс, который поможет нам создать скрипт, включает 3 метода:

  • Create — создает Script с указанным кодом и параметрами, который можно будет в последствии скомпилировать и запустить

  • RunAsync — который компилирует, выполняет переданный код и возвращает ScriptState

  • EvaluateAsync — выполняет код и возвращает результат выполнения

CSharpScript.Create можно использовать, когда вам нужно предварительно скомпилировать скрипт и часто вызывать его.

var script = CSharpScript.Create("System.Console.WriteLine(\"Hello from script\")"); script.Compile(); await script.RunAsync();

Eсли не вызвать Compile(), то код будет скомпилирован при первом вызове.

Для удобства можно создать ScriptOptions, в котором можно будет добавить namespace-ы и reference-ы (можно также добавить статические классы, на подобии using static).

  var options = ScriptOptions.Default             .AddImports("System", "System.IO", "System.Collections.Generic",                 "System.Console", "System.Diagnostics", "System.Dynamic",                 "System.Linq", "System.Text",                 "System.Threading.Tasks")             .AddReferences("System", "System.Core", "Microsoft.CSharp");   CSharpScript.Create("Console.WriteLine(\"Hello from script\")", options);

Но здесь есть один момент — ScriptOptions почему-то не ограничивают доступные namespace-ы. Этакий whitelist, как я изначально подумал, возможно, просто не до конца разобрался.

CSharpScript.RunAsync возвращает ScriptState, его можно дополнить вызвав ContinueWithAsync, который скомпилирует, выполнит код и вернет новый объект ScriptState. Можно повторно запустить скрипт, обратившись к свойству Script. Для получения результата, есть свойство ReturnValue.

ScriptState state = await CSharpScript.RunAsync("int x = 5;"); state = await state.ContinueWithAsync<int>("x + 1"); Console.WriteLine(state.ReturnValue); // 6

У объекта state можно посмотреть объявленные переменные, а так же полученный exception

foreach(var variable in state.Variables) { 	Console.WriteLine($"{variable.Name} - {variable.Value}"); }

С помощью CSharpScript.Create можно создать делегат из скрипта, который будет запускать скрипт при вызове

var script = CSharpScript.Create<Func<int,int>>("x => x+1"); Console.WriteLine(await script.CreateDelegate().Invoke(1)); // 2

А так же, можно скомпилировать лямбда-выражение в виде строки, используя CSharpScript.EvaluateAsync (или другими способами, которые были выше)

var deleg = await CSharpScript.EvaluateAsync<Func<int, int>>("x => x * 2"); Console.WriteLine(deleg(5)); // 10

Это может быть удобно для сериализации и десериализации лямбда-выражений (мой скудный ум не смог придумать юзкейса для этого, но я встречал людей, которым такая штука была нужна)

Ниже представлены тесты:

Не думаю, что они очень точны, но помогут примерно понять скорость выполнения. С кодом можете ознакомится по ссылке.

Заключение

Microsoft.CodeAnalysis.CSharp.Scripting, довольно удобная шутка, для runtime выполнения C# кода. Можно использовать например в своем движке, или для предоставления способа модификации, без надобности создания .net проекта и его сборки.

Топ 5 популярных реп в github, которые используют данный пакет:

Дополнительные примеры можно найти по ссылкам внизу:

https://github.com/dotnet/roslyn/blob/main/docs/wiki/Scripting-API-Samples.md

https://github.com/dotnet/roslyn/tree/a7319e2bc8cac34c34527031e6204d383d29d4ab/src/Scripting

Надеюсь, моя первая статья не показалось слишком скучной, и я смог как-то вам помочь.

Хорошего вам дня!

ссылка на оригинал статьи https://habr.com/ru/post/553310/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *