使用Roslyn生成代码

自从C#成为日常主要的开发语言,就觉得有时候受限于C#的语法尤其是编译器特性了。

MetaData虽说是一个非常成功的设计,C#中的Attribute可以用来做很多事情,但是似乎除了Reflection和System.Runtime.CompileServices中的功能之外,其他的就要做编译器扩展了。开发的多了这类需求就会时不时的冒出来,然而一直没有时间去接触Roslyn这个编译器前端。刚好项目里要集成了轻量级的C#解释器用来做AOT环境下的热更新(为什么不用Lua是因为想要简化整个工作流程),能够限制在C#的语言中就相对简单。

在使用解释器进行执行热更代码的基础上,构建了一个基于Protocol的调用,这样能够尽可能简化热更新代码容器和外部C#运行时的互调用。这时候一个需求是如果能够自动生成绑定的代码就更加方便了。只需要定义一个Interface的Protocol,外部runtime调用接口,内部实现接口就可以完成热更模块交互了。于是借此机会了解一下Roslyn。

初见Roslyn

目前Roslyn项目最终会生成多个Package,主要是在Microsof.CodeAnalysis的namespace下面。Roslyn提供了一套SourceCode 词法解析,生成语法树,以及给予语法树的编译对语义Semantic进行分析。同时支持各种项目结构Workspace以及一些编译器IDE的优化功能。

总体来说就是几个步骤

  1. 源代码生成语法树SyntaTree。语法树可以进行遍历获取各种语法树节点SyntaxNode
    语法树可以进行修改,遍历替换插入等等。

  2. 多个语法树加上MetaDataReference可以进行编译,生成一个Compilation对象。Compilation可以得到Diagnostic,获取编译器的报错以及各类Warning等等。

  3. Compilation中我们可以获得Semantic对象,进行语义分析。语义和语法的区别在于语法节点可以认为是一个个word,单个word没有的意义,而语义是一段段句子。事实上从我们可以从Syntax中完全可以获得到Semantic中完全相同的信息,但是Syntax中会有更多的Trivial

使用RoslynAPI

这里不想写太多琐碎的代码,就记录下使用过程中的一些理解吧。

如果你读过Roslyn项目的Overview,会注意到一个信息是在API的调用中,运用了很多的Immutable的对象。roslyn在设计上有很多东西都是不可变化的。例如从一段源代码生存了一个语法树对象,如果对这个语法树进行修改会生成一个新的Immutable的语法树对象。这样所有的节点和对象都被clone了一份,没有对象的引用使得对于一个大体量的程序来说,在使用多线程进行编译处理的时候就不需要很多线程锁和同步,由于所有操作都会生成一个immutable的对象。

下面的代码都在Microsoft.CodeAnalysis的命名空间下

1
2
3
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;

从一段代码生成一个个SyntaxTree

1
2
3
4
5
6
7
8
var sourceFile = @"
public class Program{
public void HelloWorld(){
Console.Writeline("Hello World!");
}
}
";
CSharpSyntaxTree cst = CSharpSyntaxTree.ParseText(sourceFile, CSharpParseOptions.Default, "HelloWorld.cs") as CSharpSyntaxTree;

获取到整个语法树中某个类型的SyntaxTreeNode

1
2
var root = cst.GetRoot();
Ienumerable<MethodDeclarationSyntax> methodsDeclarations = root.DescendantNodes().OfType<MethodDeclarationSyntax>();

创建一个编译对象Compilation,metaRef是对Assembly的引用,要使用Assembly的路径加载。

1
2
3
4
var metaRefSystem = MetadataReference.CreateFromFile(typeof(String).Assembly.Location);
var metaRefs = ImmutableArray.Create<MetadataReference>(metaRefSystem);

var compilation = CSharpCompilation.Create("GenerateScriptAssembly", csts, metaRefs, compilationOptions);

在得到Compilation后我们就可以获取到Semantic对象了

1
SemanticModel model = m_sourceCompilation.GetSemanticModel(m_sourceSyntaxTree,true);

SemanticModel是一个语义树,遍历里面的所有节点就是一个个Symbol了,一个Symbol会对应一个SyntaxTreeNode

到这里其实上我们就可以通过Roslyn获取到一段Source中的所有信息了,这里其实足够进行CodeGeneration了。只要把这些Method的MetaDataInfo拼在一起就可以了。

那么为什么不直接用Reflection反射获取到的信息做代码生成呢,似乎是也可以的。如果我们只需要一些方法和类的定义信息而不需要方法Body中的Statement的话。所以写到这里似乎是大材小用了,用了更加复杂强大的编译器去实现把问题复杂了。

实际需求的解决

上面只说到了语法树和语义分析,到了真正使用roslyn生成代码的时候了。roslyn提供了一个SyntaxWalkerSyntaxRewriter可以进行语法树的遍历和修改。不过最主要的工具还是SyntaxFactory这个类。这个类可以生成各种SyntaxTreeNode对象,并且通过链式调用构建整个树状结构。

贴一小段生成一个方法的代码吧

1
2
3
4
5
6
7
8
9
10
var methodSyntax = SyntaxFactory.MethodDeclaration(
SyntaxFactory.IdentifierName(SyntaxFactory.Identifier(methodData.ReturnType)), methodData.MethodName
).AddParameterListParameters(
parameters.ToArray()
).WithBody(
SyntaxFactory.Block(
SyntaxFactory.ExpressionStatement(
SyntaxFactory.InvocationExpression(SyntaxFactory.IdentifierName("Invoke"), SyntaxFactory.ArgumentList(SyntaxFactory.SeparatedList<ArgumentSyntax>())))
)
);

就是看起来特别难看的代码,由于roslyn是编译器,api自然是涉及到了每一个语言的特性,声明一个method就需要定义各种参数,方法的修饰符是什么是否是泛型,是否带有meta信息,返回值参数是否是泛型,是否是带有ref out等标记,是否有默认值的参数,parameter list等等等等。这样就导致了SyntaxFactory的API异常的复杂,完全用来拼凑一段代码异常痛苦(可能是因为我菜吧T_T)。想想在IDE里面Ctrl+.一秒钟完成的事情我写了好几个小时。

最终我放弃使用SyntaxFactory来生成代码,而用了StringBuilder。生成好代码后再使用CsharpSyntaxTree解析成语法树跑一次Compilation然后Diagnose一下,看是否有错误。整个CodeGeneration的过程就完成啦。

不过值得注意的是,直接拼接字符串生成的代码可以使用SyntaxNode.NormalizeWhitespace()来格式化代码,毕竟自己加的\t\n这种比不上Ctrl+F对吧。

总结

对于生产环境CodeGeneration来说,可能没有太多时间只需要快速实现的时候,可以试试封装过的RoslynAPI的库来进行处理,毕竟直接使用roslyn来操作太繁琐了。有时间是不是造个这样的轮子呢,脑海中已经大概有个雏形了。

总体来说开源的Roslyn给了C#和.net社区更多可能这件事是真的。我们公司的技术栈在.net这条路上远走越远啦。