[译]Roslyn – 如何创建可调试的自定义脚本语言

作者:MICHAŁ KOMOROWSKI 27/10/2016 原文链接

不久前,我决定在Cakebuild上玩一些游戏。 这是一个构建自动化工具/系统,可让您使用C#域特定语言编写构建脚本。 而且,可以在Visual Studio中调试这些脚本。 有趣的是,Cake脚本既不是“普通” C#文件,也不会添加到项目(csproj)中。 我很好奇它是如何实现的,这是我分析的结果。 我将告诉您如何创建一种简单的可调试脚本语言。 通过可调试,我的意思是可以像使用C#中的任何“常规”程序一样,在Visual Studio中以我们的语言调试脚本。 Cakebuild使用Roslyn,即Microsft提供的编译器作为服务,我们将做同样的事情。

语言

我们的示例语言将支持write命令。 这很简单,它只会向控制台中编写一些内容。 另外,也可以使用任何C#表达式。 这是一个示例脚本:

var i = 0;
write a
write 1
i++;
write 2
i++;
System.Console.WriteLine(i);
write b

翻译

我们想使用Roslyn来编译脚本。 这意味着首先我们需要将语言翻译成C#。 这是将执行此操作的代码。 它只是逐行读取。 如果一行以write开头,则将其替换为Console.WriteLine。 否则,假定一行包含C#代码,因此无需执行任何操作。

var lines = File.ReadAllLines(path);
var sb = new StringBuilder();
foreach (var l in lines)
{
   if (l.StartsWith("write"))
   {
      var res = l.Substring(l.IndexOf(" ", StringComparison.Ordinal)).Trim();
      sb.AppendLine($"System.Console.WriteLine(\"{res}\");");
   }
   else
      sb.AppendLine(l);
}

汇编

现在我们需要编译脚本。 以下片段显示了如何执行此操作(它基于Cakebuild项目中的DebugXPlatScriptSession类)。 值得注意的是,我们使用了OptimizationLevel.Debug来禁用所有优化,从而改善了调试体验。 编译的结果是两个流:assemblyStream包含实际的已编译代码,symbolStream包含符号。

var options = Microsoft.CodeAnalysis.Scripting.ScriptOptions.Default;
var roslynScript = CSharpScript.Create(script, options);
var compilation = roslynScript.GetCompilation();
 
compilation = compilation.WithOptions(compilation.Options
   .WithOptimizationLevel(OptimizationLevel.Debug)
   .WithOutputKind(OutputKind.DynamicallyLinkedLibrary));
 
using (var assemblyStream = new MemoryStream())
{
   using (var symbolStream = new MemoryStream())
   {
      var emitOptions = new EmitOptions(false, DebugInformationFormat.PortablePdb);
      var result = compilation.Emit(assemblyStream, symbolStream, options: emitOptions);
      if (!result.Success)
      {
         var errors = string.Join(Environment.NewLine, result.Diagnostics.Select(x => x));
         Console.WriteLine(errors);
         return;
      }
 
       //Execute the script
   }
}

执行

现在,我们准备执行脚本。 为此,首先需要加载由Roslyn生成的程序集和符号,然后需要找到Submission#0类和方法来进行调用。 这些奇怪的名称是由Roslyn生成的,方法的执行等同于执行我们的脚本。

var assembly = Assembly.Load(assemblyStream.ToArray(), symbolStream.ToArray());
var type = assembly.GetType("Submission#0");
var method = type.GetMethod("<Factory>", BindingFlags.Static | BindingFlags.Public);
 
method.Invoke(null, new object[] {new object[2]});

最后一步

我故意没有写一件事。 如果我们知道运行代码,脚本将正确执行。 但是,调试将无法进行,即,如果我们在脚本中创建一个断点,它将不会命中。 为什么? 因为调试器对脚本文件一无所知。

换句话说,符号与该文件之间没有链接。 为了解决这个问题,我们需要如下使用line指令。 它告诉调试器,可调试代码在给定行中开始,并且在调试时应使用给定文件。 这是执行翻译的代码段的修改版本:

...
var sb = new StringBuilder();
sb.AppendLine($"#line 1 \"{path}\"");
foreach (var l in lines)

总结

您可以在Roslyn存储库的Github上找到该工作示例。 下载后,打开解决方案,转到RoslynScriptDebugging项目,选择并打开脚本文件,然后在任何行中放置一个断点。 最后按F5键,就是这样!

发表评论

您的电子邮箱地址不会被公开。 必填项已用*标注

16 + 9 =