使用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的优化功能。
总体来说就是几个步骤
源代码生成语法树
SyntaTree
。语法树可以进行遍历获取各种语法树节点SyntaxNode
。
语法树可以进行修改,遍历替换插入等等。多个语法树加上
MetaDataReference
可以进行编译,生成一个Compilation
对象。Compilation可以得到Diagnostic
,获取编译器的报错以及各类Warning等等。从
Compilation
中我们可以获得Semantic
对象,进行语义分析。语义和语法的区别在于语法节点可以认为是一个个word,单个word没有的意义,而语义是一段段句子。事实上从我们可以从Syntax
中完全可以获得到Semantic
中完全相同的信息,但是Syntax
中会有更多的Trivial
。
使用RoslynAPI
这里不想写太多琐碎的代码,就记录下使用过程中的一些理解吧。
如果你读过Roslyn项目的Overview,会注意到一个信息是在API的调用中,运用了很多的Immutable的对象。roslyn在设计上有很多东西都是不可变化的。例如从一段源代码生存了一个语法树对象,如果对这个语法树进行修改会生成一个新的Immutable的语法树对象。这样所有的节点和对象都被clone了一份,没有对象的引用使得对于一个大体量的程序来说,在使用多线程进行编译处理的时候就不需要很多线程锁和同步,由于所有操作都会生成一个immutable的对象。
下面的代码都在Microsoft.CodeAnalysis
的命名空间下
1 | using Microsoft.CodeAnalysis.CSharp; |
从一段代码生成一个个SyntaxTree
1 | var sourceFile = @" |
获取到整个语法树中某个类型的SyntaxTreeNode
1 | var root = cst.GetRoot(); |
创建一个编译对象Compilation
,metaRef
是对Assembly的引用,要使用Assembly的路径加载。
1 | var metaRefSystem = MetadataReference.CreateFromFile(typeof(String).Assembly.Location); |
在得到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提供了一个SyntaxWalker
和SyntaxRewriter
可以进行语法树的遍历和修改。不过最主要的工具还是SyntaxFactory
这个类。这个类可以生成各种SyntaxTreeNode
对象,并且通过链式调用构建整个树状结构。
贴一小段生成一个方法的代码吧
1 | var methodSyntax = SyntaxFactory.MethodDeclaration( |
就是看起来特别难看的代码,由于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这条路上远走越远啦。