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

CodeDOM Go Kit: CodeDOM 已逝,CodeDOM 万岁

starIconstarIconstarIconstarIconstarIcon

5.00/5 (6投票s)

2019 年 12 月 11 日

MIT

9分钟阅读

viewsIcon

10594

downloadIcon

169

如果您使用 CodeDOM,这是一个不可或缺的软件包,可以使其变得非常棒。

引言

我真的很讨厌 CodeDOM,你们也应该如此。嗯,让我们先回顾一下。我喜欢 CodeDOM 的想法,但不喜欢它的实现。它笨拙、粗糙且极其有限。如果我不在乎 .NET 中语言无关的代码生成,我根本不会使用它。

然而,我确实在乎这些。我还关心利用 Microsoft 的 XML 序列化代码生成器等功能,并将其输出修改成不太糟糕的东西。

此外,Roslyn 在平台可用性和语言支持方面似乎有限,这似乎固定为 VB 和 C#。与此同时,F# 团队发布了一个 CodeDOM 提供程序,这次是为他们的语言准备的,所以看起来 CodeDOM 仍然可以生成 Roslyn 无法生成的语言的代码,并且可以在 Roslyn 目前无法运行的设备上运行。我可能对平台有所误解,也许 .NET 5 会改变一切,但这就是我们目前的状况。

以下是 CodeDOM 一直需要的一些功能:

  • 一个 Parse() 函数。天哪,那该多酷啊。
  • 一个 Eval() 函数,即使它只能执行某些表达式
  • 一个 Visit() 函数,或者其他搜索图的方法

我决定终于对此做些什么,并且随之而来的是一种非常快速、简洁地构建和编辑 CodeDOM 树的方法。

挽起袖子吧,我们要开始启动这个弗兰肯斯坦了。

构建这个大杂烩

源代码分发包在项目目录中包含两个二进制文件。它们在运行时不使用,仅在构建步骤中用于生成SlangTokenizer.cs。该工具名为 Rolex,我在“进一步阅读”部分提供了指向 CodeProject 文章的链接。这些对于项目构建来说不是必需的,但它们允许对SlangTokenizer.rl的编辑反映在相关的源文件名中。没有这个构建步骤,即使SlangTokenizer.rl发生变化,SlangTokenizer.cs也不会改变。

使用这个烂摊子

首先,让我们谈谈解析。不,我并没有为 CodeDOM 曾经渲染过的每种语言实现解析器。我所做的是为我称之为Slang的 CodeDOM 兼容的 C# 子集创建了一个解析器。用 Slang 编写的代码将解析为 CodeDOM 图,从而渲染到存在足够 CodeDOM 提供程序的任何语言。将其视为你可以转换为其他语言(如 VB)的 C# 子集。

Slang 基本上消除了构建 CodeDOM 树的大部分繁重工作。这少量魔法会让你通过以下方式创建一个表达式:

CodeExpression expr = SlangParser.ParseExpression("(a + b * c - d) > e");

你也可以解析整个编译单元——一次处理整个文件,这样你就永远不需要直接处理 CodeDOM 了。所以我们已经完成了解析。

接下来,这只适用于有限数量的用途,除非你当然想做一个计算器应用程序。但是,在我们的 CodeDomResolver 对象上,我们有一个 Evaluate() 方法,它接受 CodeExpression 形式的代码并解释它,给你结果。它可以计算、调用方法、访问属性等,但它没有任何变量或参数的概念,所以它不是一个完整的解释器,尽管稍加努力,它就可以成为一个。结合我们上面看到的,我们可以做到这一点:

var res = new CodeDomResolver();
Console.WriteLine(res.Evaluate(SlangParser.ParseExpression("5*4-7")));

将我们刚才讲过的内容结合起来,并添加一些新技巧,下面的程序将输出 13,然后是它自己的代码(但用 VB 表示):

using CD;
using System;
using System.CodeDom;

namespace CodeDomDemo
{
    class Program
    {
        static void Main(string[] args)
        {
            // evaluates a simple expression 
            var res = new CodeDomResolver();
            Console.WriteLine(res.Evaluate(SlangParser.ParseExpression("5*4-7")));

            // takes this file and converts it to vb
            var ccu = SlangParser.ReadCompileUnitFrom("..\\..\\Program.cs");
            ccu.ReferencedAssemblies.Add("CodeDomGoKit.dll");
            ccu.ReferencedAssemblies.Add(typeof(CodeObject).Assembly.GetName().ToString());
            SlangPatcher.Patch(ccu);
            Console.WriteLine(CodeDomUtility.ToString(ccu, "vb"));
        }
    }
}

像这样

Option Strict Off
Option Explicit On

Imports CD
Imports System
Imports System.CodeDom

Namespace CodeDomDemo
    Friend Class Program
        Public Shared Sub Main()
            'evaluates a simple expression
            Dim res As CodeDomResolver = New CodeDomResolver()
            System.Console.WriteLine(res.Evaluate(CD.SlangParser.ParseExpression("5*4-7")))
            'takes this file and converts it to vb
            Dim ccu As System.CodeDom.CodeCompileUnit = _
                         CD.SlangParser.ReadCompileUnitFrom("..\..\Program.cs")
            ccu.ReferencedAssemblies.Add("CodeDomGoKit.dll")
            ccu.ReferencedAssemblies.Add(GetType(CodeObject).Assembly.GetName.ToString)
            CD.SlangPatcher.Patch(ccu)
            System.Console.WriteLine(CD.CodeDomUtility.ToString(ccu, "vb"))
        End Sub
    End Class
End Namespace

现在你更没有理由再用 VB 编写任何东西了。更重要的是,现在你有了一种轻松创建 CodeDOM 图的方法。

用我最好的比利·梅斯(Billy Mays)的声音说:“还有更多!” 上面的内容并没有真正展示任何动态性,但显然,如果我们正在生成代码,我们需要生成是动态的。

为此,我提供了一个预处理器,允许进行简单的 T4 文本模板处理。这样,你就可以使用 T4(类似 ASP)的上下文切换 <# #> 来动态地从模板渲染。你的生成项目可以将该模板存储为嵌入式资源,并用它来生成代码。这是一个在文件Test.tt上使用预处理器的例子。首先,这是模板:

using System;
class Program 
{
    static void Main() 
    {
    <# 
    for(var i = 0;i<5;++i) {
    #>
        Console.WriteLine("Hello World! #<#=i+1#>");
    <#
    }
    #>
    }
}

预处理后,它产生如下输出:

using System;
class Program
{
        static void Main()
        {
                Console.WriteLine("Hello World! #1");
                Console.WriteLine("Hello World! #2");
                Console.WriteLine("Hello World! #3");
                Console.WriteLine("Hello World! #4");
                Console.WriteLine("Hello World! #5");
        }
}

请注意,输出的格式并不重要。看看我们用 Slang 运行这个并输出到 C# 时会发生什么:

using System;

internal class Program {
    public static void Main() {
        System.Console.WriteLine("Hello World! #1");
        System.Console.WriteLine("Hello World! #2");
        System.Console.WriteLine("Hello World! #3");
        System.Console.WriteLine("Hello World! #4");
        System.Console.WriteLine("Hello World! #5");
    }
}

不仅格式更好,而且它对程序进行了一些更改。首先,我们的类型已完全限定。其次,我们的 Main() 方法被设为公共!这是因为它被检测为入口点方法,所以 SlangPatcher.Patch() 会用 CodeEntryPointMethod 来表示它。当 CodeDOM 渲染此类时,它总是将可见性设置为 public。我们无法直接控制这一点。CodeDOM 也不支持入口点方法的参数或返回值,很遗憾。

总之,以下是实现这一目标的 T4 代码,假设Test.tt在我们的项目目录中:

var sw = new StringWriter();
using (var sr = new StreamReader(@"..\..\Test.tt"))
    SlangPreprocessor.Preprocess(sr, sw);
var ccu = SlangParser.ParseCompileUnit(sw.ToString());
SlangPatcher.Patch(ccu);
Console.WriteLine(CodeDomUtility.ToString(ccu));

如果我们想美化我们得到的树,也许对一些代码进行搜索和替换,我们有 CodeDomVisitor

/// now let's take our code and modify it
CodeDomVisitor.Visit(ccu,(ctx) => {
    // we're looking for a method invocation
    var mi = ctx.Target as CodeMethodInvokeExpression;
    if (null != mi)
    {
        // ... calling WriteLine
        if ("WriteLine" == mi.Method?.MethodName)
        {
            // replace the passed in expression with "Hello world!"
            mi.Parameters.Clear();
            mi.Parameters.Add(new CodePrimitiveExpression("Hello world!"));
            // done after the first WriteLine so we cancel
            ctx.Cancel = true;
        }
    }
});

我上面没有展示它,因为我们不需要它,但在需要替换自己的目标的情况下,CodeDomVisitor 有一个非常有创意的名字的 ReplaceTarget() 方法,它只接收你当前的上下文和新对象。它使用反射来发挥其“替换自身”的魔力,设置适当父级的属性或替换父级集合中的项,视情况而定。

现在,我们已经简要地提到了 CodeDomResolver,但如果你要做任何超级花哨的 CodeDOM 操作,比如编写一个编译器或一个解释器并用它来存放你的代码,你可以使用这个类来为你提供树的类型和范围信息。SlangPatcher 广泛使用这个类。

// create one of these lil guys
var res = new CodeDomResolver();
// add our code to it
res.CompileUnits.Add(ccu);
// give it a chance to build its information over our code
res.Refresh();
CodeDomVisitor.Visit(ccu, (ctx) => {
    // for every expression...
    var expr = ctx.Target as CodeExpression;
    if(null!=expr)
    {
        // except method reference expressions...
        var mri = expr as CodeMethodReferenceExpression;
        if (null != mri)
            return;
        // get the expression type
        var type = res.TryGetTypeOfExpression(expr);
        // write it along with the expression itself
        Console.WriteLine(
            "Expression type {0}: {1} is {2}",
            expr.GetType().Name,
            CodeDomUtility.ToString(expr),
            null!=type?CodeDomUtility.ToString(type):"unresolvable");
    }
});

这会输出以下内容:

Expression type CodeObjectCreateExpression: new CodeDomResolver() is CodeDomResolver
Expression type CodeMethodInvokeExpression: System.Console.WriteLine("Hello world!") is void
Expression type CodeTypeReferenceExpression: System.Console is System.Console
Expression type CodePrimitiveExpression: "Hello world!" is string
Expression type CodeMethodInvokeExpression: 
  CD.SlangParser.ReadCompileUnitFrom("..\\..\\Demo1.cs") is System.CodeDom.CodeCompileUnit
Expression type CodeTypeReferenceExpression: CD.SlangParser is CD.SlangParser
Expression type CodePrimitiveExpression: "..\\..\\Demo1.cs" is string
Expression type CodeMethodInvokeExpression: 
  ccu.ReferencedAssemblies.Add("CodeDomGoKit.dll") is int
Expression type CodePropertyReferenceExpression: 
  ccu.ReferencedAssemblies is System.Collections.Specialized.StringCollection
Expression type CodeVariableReferenceExpression: ccu is System.CodeDom.CodeCompileUnit
Expression type CodePrimitiveExpression: "CodeDomGoKit.dll" is string
Expression type CodeMethodInvokeExpression: 
  ccu.ReferencedAssemblies.Add(typeof(CodeObject).Assembly.GetName().ToString()) is int
Expression type CodePropertyReferenceExpression: 
  ccu.ReferencedAssemblies is System.Collections.Specialized.StringCollection
Expression type CodeVariableReferenceExpression: ccu is System.CodeDom.CodeCompileUnit
Expression type CodeMethodInvokeExpression: 
  typeof(CodeObject).Assembly.GetName().ToString() is string
Expression type CodeMethodInvokeExpression: 
  typeof(CodeObject).Assembly.GetName() is System.Reflection.AssemblyName
Expression type CodePropertyReferenceExpression: 
  typeof(CodeObject).Assembly is System.Reflection.Assembly
Expression type CodeTypeOfExpression: typeof(CodeObject) is System.Type
Expression type CodeMethodInvokeExpression: CD.SlangPatcher.Patch(ccu) is unresolvable
Expression type CodeTypeReferenceExpression: CD.SlangPatcher is CD.SlangPatcher
Expression type CodeVariableReferenceExpression: ccu is System.CodeDom.CodeCompileUnit
Expression type CodeMethodInvokeExpression: 
  System.Console.WriteLine(CD.CodeDomUtility.ToString(ccu, "vb")) is void
Expression type CodeTypeReferenceExpression: System.Console is System.Console
Expression type CodeMethodInvokeExpression: CD.CodeDomUtility.ToString(ccu, "vb") is string
Expression type CodeTypeReferenceExpression: CD.CodeDomUtility is CD.CodeDomUtility
Expression type CodeVariableReferenceExpression: ccu is System.CodeDom.CodeCompileUnit
Expression type CodePrimitiveExpression: "vb" is string
Expression type CodeMethodInvokeExpression: 
  System.Console.WriteLine("Press any key...") is void
Expression type CodeTypeReferenceExpression: System.Console is System.Console
Expression type CodePrimitiveExpression: "Press any key..." is string
Expression type CodeMethodInvokeExpression: System.Console.ReadKey() is System.ConsoleKeyInfo
Expression type CodeTypeReferenceExpression: System.Console is System.Console
Expression type CodeMethodInvokeExpression: System.Console.Clear() is void
Expression type CodeTypeReferenceExpression: System.Console is System.Console
Expression type CodeVariableReferenceExpression: ccu is System.CodeDom.CodeCompileUnit

有一个表达式它无法解析。无法解析的原因是因为它调用了一个方法 SlangPatcher.Patch(),该方法接受一个 params 数组参数,而我没有编写支持代码来使其工作,即使它听起来很难,但也很难。

我们还需要一种方法来解析 CodeTypeReference 对象,这就是为什么解析器有一个 TryResolveType(),它尝试检索 CodeTypeReference 所代表的类型。根据来源,它可能是一个运行时 Type,或者它可能是一个 CodeTypeDeclaration。如果无法解析,它将为 null。目前,没有一个替代方法会抛出异常。

这没问题,但是如果我们想从声明的类型中提取成员呢?这就是 CodeDomReflectionBinder 的用途。它的工作方式类似于 Microsoft 的 DefaultBinder,但它同时处理 CodeDOM 对象和运行时类型。基本上,这个类根据名称和签名进行成员发现和选择。

// once again, we need one of these
var res = new CodeDomResolver();
res.CompileUnits.Add(ccu);
res.Refresh();
// we happen to know Program is the 1st type in the 2nd namespace*
var prg = ccu.Namespaces[1].Types[0];
// we need the scope where we're at
var scope = res.GetScope(prg);
// because our binder attaches to it
var binder = new CodeDomReflectionBinder(scope);
// now get all the methods with the specified name and flags
var members = binder.GetMethodGroup
              (prg, "TestOverload",BindingFlags.Public | BindingFlags.Static);
Console.WriteLine("There are {0} TestOverload method overloads.", members.Length);
// try selecting one that takes a single string parameter
var argTypes1 = new CodeTypeReference[] { new CodeTypeReference(typeof(string)) };
var m = binder.SelectMethod
        (BindingFlags.Public | BindingFlags.Static, members, argTypes1, null);
if (null != m)
{
    Console.WriteLine("Select TestOverload(string) returned:");
    _DumpMethod(m);
}
else
    Console.WriteLine("Unable to bind to method");
// try selecting one that takes a single it parameter
var argTypes2 = new CodeTypeReference[] { new CodeTypeReference(typeof(int)) };
m = binder.SelectMethod(BindingFlags.Public | BindingFlags.Static, members, argTypes2, null);
if (null != m)
{
    Console.WriteLine("Select TestOverload(int) returned:");
    _DumpMethod(m);
}
else
    Console.WriteLine("Unable to bind to method");

这会输出以下内容:(参考Demo1.cs

There are 2 TestOverload method overloads.
Select TestOverload(string) returned:
public static int TestOverload(string val) {
    System.Console.WriteLine(val);
    return val.GetHashCode();
}

Select TestOverload(int) returned:
public static string TestOverload(int val) {
    System.Console.WriteLine(val);
    return val.ToString();
}

正如你所见,我们根据签名成功地选择了每个方法重载。

你会注意到 Binder 在很多地方都使用 object 类型。这是由于类的双重性质。它在反射的运行时类型和代码对象上运行,所以为了接受或返回这两种类型,它们必须被表示为 object。你必须为每种类型进行测试和转换才能弄清楚它是什么,而上面的 _DumpMethod() 所做的就是这些,尽管它没有显示出来。在这种情况下,它们碰巧都是代码中声明的方法,但如果我们的类派生自具有这些方法的运行时类型,我们将收到 MethodInfo 实例而不是 CodeMemberMethod

最后,我们一直在大量使用 CodeDomUtility,但我们还没有真正介绍它。它只是一堆用于创建各种 CodeDOM 构造的缩写。如果 Slang 是自动变速器,那么 CodeDomUtility 就是手动挡。更难使用,但控制更多。

var state = CDU.FieldRef(CDU.This, "_state");
var current = CDU.FieldRef(CDU.This, "_current");
var line = CDU.FieldRef(CDU.This, "_line");
var column = CDU.FieldRef(CDU.This, "_column");
var position = CDU.FieldRef(CDU.This, "_position");

var enumInterface = CDU.Type(typeof(IEnumerator<>));
enumInterface.TypeArguments.Add("Token");
var result = CDU.Method(typeof(bool), "MoveNext", MemberAttributes.Public);
result.ImplementationTypes.Add(enumInterface);
result.ImplementationTypes.Add(typeof(System.Collections.IEnumerator));
result.Statements.AddRange(new CodeStatement[]
{
    CDU.If(CDU.Lt(state,CDU.Literal(_Enumerating)),
        CDU.If(CDU.Eq(state,CDU.Literal(_Disposed)),
            CDU.Call(CDU.TypeRef("TableTokenizerEnumerator"),"_ThrowDisposed")),
        CDU.If(CDU.Eq(state,CDU.Literal(_AfterEnd)),CDU.Return(CDU.False))),
    CDU.Let(current,CDU.Default("Token")),
    CDU.Let(CDU.FieldRef(current,"Line"),line),
    CDU.Let(CDU.FieldRef(current,"Column"),column),
    CDU.Let(CDU.FieldRef(current,"Position"),position),
    CDU.Call(CDU.FieldRef(CDU.This,"_buffer"),"Clear"),
    CDU.Let(CDU.FieldRef(current,"SymbolId"),CDU.Invoke(CDU.This,"_Lex")),
    CDU.Var(CDU.Type(typeof(bool)),"done",CDU.False),
    CDU.While(CDU.Not(CDU.VarRef("done")),
        CDU.Let(CDU.VarRef("done"),CDU.True),
        CDU.If(CDU.Lt(CDU.Literal(_ErrorSymbol),CDU.FieldRef(current,"SymbolId")),
            CDU.Var(typeof(string),"be",CDU.ArrIndexer(CDU.FieldRef(CDU.This,"_blockEnds"),
                    CDU.FieldRef(current,"SymbolId"))),
            CDU.If(CDU.Not(CDU.Invoke
                  (CDU.TypeRef(typeof(string)),"IsNullOrEmpty",CDU.VarRef("be"))),
                CDU.If(CDU.Not(CDU.Invoke(CDU.This,"_TryReadUntilBlockEnd",CDU.VarRef("be"))),
                    CDU.Let(CDU.FieldRef(current,"SymbolId"),CDU.Literal(_ErrorSymbol)))
                )),
                CDU.If(CDU.And(CDU.Lt(CDU.Literal(_ErrorSymbol),
                       CDU.FieldRef(current,"SymbolId")),CDU.NotEq
                       (CDU.Zero,CDU.BitwiseAnd(CDU.ArrIndexer
                       (CDU.FieldRef(CDU.This,"_nodeFlags"),
                        CDU.FieldRef(current,"SymbolId")),CDU.One))),
                CDU.Let(CDU.VarRef("done"),CDU.False),
                CDU.Let(CDU.FieldRef(current,"Line"),line),
                CDU.Let(CDU.FieldRef(current,"Column"),column),
                CDU.Let(CDU.FieldRef(current,"Position"),position),
                CDU.Call(CDU.FieldRef(CDU.This,"_buffer"),"Clear"),
                CDU.Let(CDU.FieldRef(current,"SymbolId"),CDU.Invoke(CDU.This,"_Lex")))
        ),
    CDU.Let(CDU.FieldRef(current,"Value"),CDU.Invoke(CDU.FieldRef
           (CDU.This,"_buffer"),"ToString")),
    CDU.If(CDU.Eq(CDU.FieldRef(current,"SymbolId"),CDU.Literal(_EosSymbol)),
        CDU.Let(state,CDU.Literal(_AfterEnd))),
    CDU.Return(CDU.NotEq(state,CDU.Literal(_AfterEnd))) });
return result;

如果你仔细看,你可以大致看到它里面创建的 CodeDOM 结构。基本上,它正在实现一个 IEnumerator<Token>.MoveNext() 方法。

这个类有两个普遍感兴趣的方法,即使你从不使用它来生成上面这样的代码:ToString(),它可以将几乎任何 CodeDOM 对象呈现为字符串;以及 Literal(),它可以将原始类型、数组和对象**序列化为 CodeDOM 结构。这对于生成表代码(如 DFA 状态表和解析表)非常有用,这些表通常存储在嵌套数组中。你的生成器只需要实例化数组的“实时”版本,然后将其传递给 Literal(array) 来获取可用于再次创建它的 CodeExpression 对象。这可以成为一个静态字段初始化程序,用于存储你预生成的表。同样,这非常有用,我在许多代码生成项目中都使用了它。如果你要生成大型数组,这样比使用 Slang 更高效、更容易。

(**需要适当的 InstanceDescriptor/TypeConverter 设置)

限制

虽然 Slang 越来越好,但它仍然存在一些未解决的问题,并且基本上是实验性的。如果它适用于你的代码,那就太好了。如果不适用,那么希望稍后的修订版能解决这个问题。错误处理也需要大量工作。

有些东西 Slang 永远无法支持,比如后置增量和减量运算符、带参数的入口点方法、try-cast、实例化多维数组或调用大多数运算符重载。这些是基于 CodeDOM 的限制,所以我对此无能为力。

Binder 和 Resolver 还没有完成,并且在处理嵌套类型和某些泛型用法时可能遇到问题,并且不支持使用可选参数或 param 数组进行绑定。

延伸阅读

历史

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