65.9K
CodeProject 正在变化。 阅读更多。
Home

Deslang:从代码到 CodeDOM 再到代码

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.20/5 (4投票s)

2019 年 12 月 15 日

MIT

9分钟阅读

viewsIcon

5526

downloadIcon

129

代码生成,更快

Deslanged code

引言

请注意,该项目是我更大的项目“Build Pack”的一部分,它是一套用于构建构建工具的实用程序和工具——通常是源代码生成器。

其中许多工具都由 Slang 提供支持,这项技术允许 C# 代码的一个子集被渲染成各种潜在的 .NET 语言,当然也包括 VB.NET。这使得使用 Slang 的代码生成器工具无需担心目标项目的构建语言即可渲染代码。它可以很好地支持 VB.NET 项目、C# 项目,甚至可能是 F# 项目。

Deslang 被编写为一种优化工具,但它的可能性绝不仅限于此。它被编写出来是为了缩小和加速那些在运行时不需要 Slang 全部功能的 Slang 驱动项目。

即使您不使用 Slang,Deslang 可能也很有用,但 Deslang 是作为 Slang 的伴侣编写的。

Deslang 允许您将 .NET 代码的语言中立/不可知表示形式存储在代码中的静态字段中。这些静态字段包含完全实例化的 CodeCompileUnit/CodeDOM 对象,这些对象代表您输入到此构建工具的代码。如果这段代码是作为 Slang 的一部分生成的,但这又不希望每次都通过 Slang 引擎处理代码,那么这会非常有用。例如,如果您有使用 Slang 编写的静态库代码,并将其作为工具执行的一部分导出,那么您可以通过将该静态代码预先构建到工具的静态字段中来缩小工具体积并提高其速度。像这样预先构建的代码不需要 Slang 引擎即可重新实例化。它不需要运行 Slang 来解析和解析代码,而是将代码存储为完全解析的 CodeDOM 对象,随时可以输出到任何目标语言。

即使纯粹作为一种好奇心,该工具也是一项很酷的技术,因为它将 C# 代码变成了用于渲染该代码(到 CodeDOM 提供程序)的代码。

背景

Slang 对于代码生成工具的开发者来说是一个巨大的胜利,因为他们不必使用 CodeDOM 来编写语言中立的代码生成,Slang 允许他们使用 C# 的一个子集编写代码,然后可以将该代码转换为任何有 CodeDOM 的目标语言。

现在,您的代码生成代码不再是丑陋的 CodeDOM 代码,而是直接用 C# 编写。用 C# 编写,它会自动渲染成 VB 或 C#。

然而,缺点是它在资源方面并非免费。CodeDOM 中的解析和类型解析非常耗时,并且会占用 Slang 大量的 CPU 周期。它并不消耗太多内存,但在某些时候会消耗大量 CPU。它还使得 Slang 的二进制文件大小在编译(发布)后约为 200k,即使它被嵌入到其他项目中。

Deslang 允许您基本上将 Slang 的魔力“罐装”起来以备将来使用,因此您仍然可以使用 Slang,但可以“减肥”。

它输出的字段如下所示(以 Token.cs 为例,下面会详细介绍)

public static System.CodeDom.CodeCompileUnit Token = 
  Shared._CompileUnit(new string[0], new CodeNamespace[] {
  Shared._Namespace("Rolex", new CodeNamespaceImport[0], new CodeTypeDeclaration[] {
  Shared._TypeDeclaration("Token", false, false, false, true, false, 
  ((MemberAttributes)(0)), TypeAttributes.NotPublic, new CodeTypeParameter[0], 
  new CodeTypeReference[0], new CodeTypeMember[] {
  Shared._MemberField(new CodeTypeReference("System.Int32"), 
  "Line", null, (MemberAttributes.Final | MemberAttributes. Public), 
  new CodeCommentStatement[] {
    new CodeCommentStatement(new CodeComment("<summary>", true)),
    new CodeCommentStatement(new CodeComment
("Indicates the line where the token occurs", true)),
    new CodeCommentStatement(new CodeComment
("</summary>", true))}, new CodeAttributeDeclaration[0], 
new CodeDirective[0], new CodeDirective[0], null),
  Shared._MemberField(new CodeTypeReference("System.Int32"), 
  "Column", null, (MemberAttributes.Final | MemberAttributes. Public), 
  new CodeCommentStatement[] {
    new CodeCommentStatement(new CodeComment
("<summary>", true)),
    new CodeCommentStatement(new CodeComment
("Indicates the column where the token occurs", true)),
    new CodeCommentStatement(new CodeComment("</summary>", true))}, 
new CodeAttributeDeclaration[0], new CodeDirective[0], new CodeDirective[0], null),
  Shared._MemberField(new CodeTypeReference("System.Int64"), "Position", 
null, (MemberAttributes.Final | MemberAttributes. Public), new CodeCommentStatement[] {
    new CodeCommentStatement(new CodeComment("<summary>", true)),
    new CodeCommentStatement(new CodeComment
("Indicates the position where the token occurs", true)),
    new CodeCommentStatement(new CodeComment("</summary>", true))}, 
new CodeAttributeDeclaration[0], new CodeDirective[0], new CodeDirective[0], null),
  Shared._MemberField(new CodeTypeReference("System.Int32"), "SymbolId", 
null, (MemberAttributes.Final | MemberAttributes. Public), new CodeCommentStatement[] {
    new CodeCommentStatement(new CodeComment("<summary>", true)),
    new CodeCommentStatement(new CodeComment
("Indicates the symbol id or -1 for the error symbol", true)),
    new CodeCommentStatement(new CodeComment("</summary>", true))}, 
new CodeAttributeDeclaration[0], new CodeDirective[0], new CodeDirective[0], null),
  Shared._MemberField(new CodeTypeReference("System.String"), "Value", 
null, (MemberAttributes.Final | MemberAttributes. Public), new CodeCommentStatement[] {
    new CodeCommentStatement(new CodeComment("<summary>", true)),
    new CodeCommentStatement(new CodeComment("Indicates the value of the token", true)),
    new CodeCommentStatement(new CodeComment("</summary>", true))}, 
new CodeAttributeDeclaration[0], new CodeDirective[0], new CodeDirective[0], null)}, 
new CodeCommentStatement[] {
    new CodeCommentStatement(new CodeComment("<summary>", true)),
    new CodeCommentStatement(new CodeComment
("Reference implementation for generated shared code", true)),
    new CodeCommentStatement(new CodeComment("</summary>", true))}, 
    new CodeAttributeDeclaration[0], new CodeDirective[0], new CodeDirective[0], null)}, 
    new CodeCommentStatement[0])}, new CodeAttributeDeclaration[0], 
    new CodeDirective[0], new CodeDirective[0]);

看,这是一个 CodeDOM 图,包含了一堆代码!它并不好看,但别担心。它生成的东西足够漂亮——这是上面渲染成 C# 的输出

namespace Rolex
{
    /// <summary>
    /// Reference implementation for generated shared code
    /// </summary>
    struct Token
    {
        /// <summary>
        /// Indicates the line where the token occurs
        /// </summary>
        public int Line;
        /// <summary>
        /// Indicates the column where the token occurs
        /// </summary>
        public int Column;
        /// <summary>
        /// Indicates the position where the token occurs
        /// </summary>
        public long Position;
        /// <summary>
        /// Indicates the symbol id or -1 for the error symbol
        /// </summary>
        public int SymbolId;
        /// <summary>
        /// Indicates the value of the token
        /// </summary>
        public string Value;
    }
}

现在这是漂亮的、格式良好的代码,并且包含了注释。这就是上面那个丑陋的 Token 字段包含的内容。所以,如果我们想将它呈现给最终用户,我们可以将该字段传递给 GenerateCodeFromCompileUnit()。它在 VB 或 F# 中看起来同样漂亮,不用担心。

但通常情况下,我不会直接将静态代码呈现给用户。那不太有用。我经常使用 Deslang 以这种方式存储的代码,在将其交给下游消费者之前,使用 CodeDomVisitor 进行修改。这样,我就可以在不需要所有 Slang 和 CodeDOM Go Kit 的情况下获得我的动态性。如前所述,省略 Slang 可以将最终分发的大小减少约 200k,并加快应用程序速度。我只是通过包含其源文件来使用访问者功能,这只会为最终二进制文件增加约 30k。

构建这个大杂烩

该解决方案使用自己的输出来构建自身。您没听错。另外,由于我删除了 zip 文件中的二进制文件,您需要先执行一次Release构建。您需要构建两到三次,直到错误消失,因为 Visual Studio 喜欢在文件关闭之前移动文件。它最终会构建成功。如果不行,说明您忘记将其切换到“Release”。完成后,再将其切换回 debug。原因是项目中有几个预构建步骤,它们使用其他项目的 release 二进制文件来构建它们的源代码。

例如,Rolex 使用 Deslang 生成其模板和库代码,而 CodeDomGoKit 使用 Rolex 来构建 Slang 的分词器(是的,这是循环的,但这并不重要)。RolexDemo 使用 Rolex 来构建其用于解析器的示例分词器。

使用这个烂摊子

所以,使用这个,我们可以声明如下(在 Widget.cs 中)

using System;
namespace CorporateHellscape
{
    /// <summary>
    /// Base widget implementation
    /// </summary>
    partial class Widget 
    {
        // our payload - to be filled
        byte[] _payload;

        public override string ToString()
        {
            return "[Widget - " + Convert.ToBase64String(_payload) +"]";
        }
    }
}

以上是我们的实现静态部分。我们将修改这段代码,为 _payload 字段赋值。

首先,让我们生成这个的 deslanged 版本。

deslang Widget.cs /output DeslangedWidget.cs /namespace DeslangedDemo /ifstale

这已经是包含的 DeslangDemo 项目中的一个预构建步骤。

现在,通过包含 DeslangedWidget.cs 在我们的项目中,我们可以使用以下方式访问 Widget 代码的 CodeDOM

CodeCompileUnit code = Deslanged.Widget;

其中 code 是我们的编译单元。

现在,我们可以转向 CodeDOM Go KitCodeDomVisitor,我们将使用它来 Visit()

CodeDomVisitor.Visit(Deslanged.Widget, (ctx) =>
{
    // look for our _payload field
    var f = ctx.Target as CodeMemberField;
    if(null!=f && "_payload"==f.Name)
    {
        // give it some data
        f.InitExpression = CodeDomUtility.Literal(_Hash(DateTime.UtcNow.ToString()));
        // we're done searching
        ctx.Cancel = true;
    }
});

现在如果我们把 Deslanged.Widget 转储到控制台,我们会看到 _payload 字段已经被填充。请注意,我们将它标记为 readonly,但 CodeDOM 不支持这一点,Slang 也不支持,很遗憾。

这是一个非常简单、人为设计的例子。然而,Rolex 更贴近现实世界,并广泛使用了这个功能,不仅包含三个 deslanged 文件,并选择性地包含在生成的输出中,而且还包含两个 deslanged 文件,它们像上面一样进行修改以生成其代码。 老版本的 Rolex 没有 Slang,所以你可以比较一下替代方案有多复杂

// constructor
var ctor = new CodeConstructor();
ctor.Attributes = MemberAttributes.Public;
ctor.Parameters.Add(new CodeParameterDeclarationExpression(typeof(IEnumerable<char>), "input"));
ctor.BaseConstructorArgs.Add(new CodeFieldReferenceExpression(null, dfaTableField.Name));
ctor.BaseConstructorArgs.Add(new CodeFieldReferenceExpression(null, blockEndsField.Name));
ctor.BaseConstructorArgs.Add(new CodeFieldReferenceExpression(null, nodeFlagsField.Name));
ctor.BaseConstructorArgs.Add(new CodeArgumentReferenceExpression(ctor.Parameters[0].Name));
result.Members.Add(ctor);

那只是用来声明一个构造函数。声明类需要一整页代码,所以我省略了所有内容,只保留了上面的内容。现在,在后期版本的 Rolex 中,我们使用 C# Slang 子集声明整个类

using System.Collections.Generic;

namespace Rolex
{
    class TableTokenizerTemplate : TableTokenizer
    {
        internal static DfaEntry[] DfaTable; // to be populated
        internal static int[] NodeFlags; // to be populated
        internal static string[] BlockEnds; // to be populated
        // this was what the above code declared:
        public TableTokenizerTemplate(IEnumerable<char> input) :
               base(DfaTable, BlockEnds, NodeFlags, input)
        {
        }
    }
}

显然,我们需要填充这些字段,并将类名更改为用户选择的名称,但我们已经涵盖了使用访问者更新字段。更新三个字段和一个类名并不复杂

CodeDomVisitor.Visit(Shared.TableTokenizerTemplate, (ctx) => {
    td = ctx.Target as CodeTypeDeclaration;
    if(null!=td && td.Name.EndsWith("Template"))
    {
        // we need the original name for later but not here
        origName += td.Name;
        td.Name = name;
        var f = CodeDomUtility.GetByName("DfaTable", td.Members) as CodeMemberField;
        f.InitExpression = CodeGenerator.GenerateDfaTableInitializer(dfaTable);
        f = CodeDomUtility.GetByName("NodeFlags", td.Members) as CodeMemberField;
        f.InitExpression = CodeDomUtility.Literal(nodeFlags);
        f = CodeDomUtility.GetByName("BlockEnds", td.Members) as CodeMemberField;
        f.InitExpression = CodeDomUtility.Literal(blockEnds);
        CodeGenerator.GenerateSymbolConstants(td, symbolTable);
        ctx.Cancel = true;
    }
});

基本上我们所做的是,我们找到源文件中以“Template”结尾的第一个类型声明,然后从那里开始,找到每个字段并设置它们的值,有点像我们对 Widget 所做的那样,我们还调用 CodeGenerator.GenerateSymbolConstants() 与我们的类一起将常量名称放在上面。如果我们使用的是完整的 Slang,而不是 deslanged 文件,我们可以使用 T4 预处理器来渲染它们,那将是很好的,但使用 CodeDomUtility.Field() 并不是那么糟糕。无论哪种方式,我们都必须确保没有名称冲突,并且标识符是有效的,这就是该例程的功能,为什么它不直接返回一个字段数组。

请注意,我们不能使用 CodeDomUtility.Literal() 来序列化我们的 DFA 状态表,因为数组包含结构体,而我们实际上还没有定义它们(至少还没有以二进制形式),所以我们自己序列化。

我们还必须确保更新我们的类型引用——简单地将 Rolex.TableTokenizerTemplate 的旧名称替换为我们最终类的类型。我们在下面的最终访问传递中执行此操作。我应该展示它,但它很简单。

万岁!现在我们有了新的生成例程,它由源模板文件 TableTokenizerTemplate.cs 支持,并且对它的更改应该仍然允许其余的代码正常工作,只要关键部分到位。所有这些都不需要 Rolex 工具本身运行 Slang。

限制和注意事项

请记住关闭用于生成的 Slang 文件的编译。例如,Rolex 关闭了其 Shared 文件夹中所有内容的编译,因为那些只是构建过程使用的 C# 模板。它们不会被编译到这个项目中。从这个角度考虑可能会令人困惑。简单的思考方式是——不要编译 Slang 输入文件!——包括任何馈送给 Deslang 的文件。

请记住,Slang 仍然在某种程度上是实验性的,您的代码可能无法与其一起工作。一般的规则是,保持简单,尽量不要做 CodeDOM 不允许您做的事情。嵌套类和泛型类支持相当不稳定,并且不支持显式基类引用。此外,有时它就是会变得脾气不好。构建时请阅读 deslang 输出。如果 Slang 无法解析某些内容,它会警告您。您可能需要调整您的源代码才能正确运行。Slang 目前每天都在改进,所以每一天都能处理更多的代码。

请记住,将所有需要协同工作的编译单元馈送给 Deslang。如果一个使用了另一个的代码,它们都需要在同一次传递中被 deslang 读取,以便 Slang 能够解析它们。这可能意味着馈送 deslang 多个输入,即使您不打算使用所有输出。

请记住使用 /asms 引用您在文件中使用的程序集。

请记住,一旦您修改了在 Deslanged(或替代命名的)类下的代码树,您将无法恢复到原始版本,直到应用程序下次运行时。

请记住,您可以包含 CodeDOM Go Kit 中的 CodeDomVisitor.csCodeDomUtility.cs 来对 **deslang** 为您准备的代码树进行代码修改。

关注点

整个项目有点古怪。首先,它生成生成代码的代码。它可能有点难以理解。但是,它很容易上手。

历史

  • 2019 年 12 月 12 日 - 初始提交
© . All rights reserved.