文章转载授权级别:A
一 、 引言
Natasha 距离上个 2.+ 版本大概有1个月了,在4月份里我把模板与引擎进行了重构,旨在更抽象、规范、合理,方便其他人参与开源、定制。接下来我将从 引擎的结构 、类库的使用及新热的 Source Generators 技术进行一些讲解:
二 、 Natasha.Framework
框架结构示意图:
【FrameworkAPI 预览】https://natasha.dotnetcore.xyz/zh/framework/framework.html
三大基础类:
1、域操作类 DomainBase , 在动态开发的环境中,域尤为重要,这是隔离 程序集污染 的重要手段,也是你反复折腾编译的基本单元容器。而 DomainManagement 则是我封装的一个认为比较合理的 域管理操作类 ,至少可以让开发者对域的弱管理这块操心少一点。
2、语法操作类 SyntaxBase , 如果说域是基本单元容器,那么 程序集 就是你编译的基本单元,组成程序集可以是很多个类,枚举,接口等等,它们在动态编译流程中是以 SyntaxTree 形式存在,编译时这些 SyntaxTree 最终将会编译到一个程序集中。
一个很简单的流程就是:字符串 -> 树 -> 某域中的程序集 。
同时你还需要知道,C# 有 C#的语法树(CSharpSyntaxTree),VB 有 VB的语法树 (VisualBasicSyntaxTree)等,SyntaxTree 作为抽象标准,您只需实现方法并制造返回你需要的语法树即可。
3、编译操作类 CompilerBase, 这个类抽象出了编译行为,不管是 C# 还是 VB,只要你实现方法并返回你需要的语言的编译信息即可。之所以同一个方法能返回不同的编译信息,是因为它们都是继承自 Compilation 类,这个类是基础的是编译的关键所在(包括前两天挺热的 Source Generators 技术)。
三 、 实现
如图所示:
图中将上面所述的三大基础类各自实现了 CSharp 的功能并封装成库,Natasha 的 C# 编译引擎由这几个库构成,它们在 CSharpEngine 库中完成协作,编译的流程在 CSharpEngine 中得到了更好、更严格的控制。
四、 API 与 Samples
Natasha 3.0 在 API 上做出了一些更为标准和舒适的改变:
原快速构建委托的 NDomain 更名为 NDelegate
原基础 C# 编译器 AssemblyCompiler 更名为 AssemblyCSharpBuilder
如何开始:
引入 DotNetCore.Natasha 库(最新版为3.0.0.1, 修复了跨平台的BUG);
引入 编译环境库 :DotNetCore.Compile.Environment ;
向引擎中注入定制的域:DomainManagement.RegisterDefault< AssemblyDomain >();
敲代码;
下面我将拿出一些例子来展示动态编译的基本操作:
静态初始化:
在静态构造上进行了复用与改进:
Natasha 的所有模板均继承自 CompilerTemplate ,CompilerTemplate 本身会提供静态构造方法。因此上层 API 也会被支持。
NDelegate / NAssembly / NClass.. / xxx_Oerator 等等均拥有该静态构造方法,以下称为 “Handler”.
//使用 domain 域
Handler.UseDomain(domian, compiler => { 编译器配置 });//使用某编译器的域
Handler.UseCompiler(assemblyCSharpBuilder, compiler => { 编译器配置 }));//创建一个叫 "domainJim" 域
Handler.CreateDomain("domianJim", compiler => { 编译器配置 });//使用默认域
Handler.DefaultDomain(compiler => { 编译器配置 });//使用随机域
Handler.RandomDomain(compiler => { 编译器配置 });
编译器配置:
builder =>
{builder.CustomerUsing() //使用用户自定义的Using.SetAssemblyName("MyAssemblyName") //设置程序集名.ThrowAndLogCompilerError() //抛出并记录编译器的异常.ThrowSyntaxError() //抛出语法树异常.UseStreamCompile(); //使用流编译
}
字符串编译:
//引擎开放之后,您可以向引擎中注入自己实现的域
//这里的 AssemblyDomain 是 Natasha 实现的域
DomainManagement.RegisterDefault<AssemblyDomain>();//使用 Natasha 的 CSharp 编译器直接编译字符串
AssemblyCSharpBuilder sharpBuilder = new AssemblyCSharpBuilder();//给编译器指定一个随机域
sharpBuilder.Compiler.Domain = DomainManagement.Random;//使用文件编译模式,动态的程序集将编译进入DLL文件中
//当然了你也可以使用内存流模式。
sharpBuilder.UseFileCompile();//如果代码编译错误,那么抛出并且记录日志。
sharpBuilder.ThrowAndLogCompilerError();
//如果语法检测时出错,那么抛出并记录日志,该步骤在编译之前。
sharpBuilder.ThrowAndLogSyntaxError();//添加你的字符串
sharpBuilder.Syntax.Add(@"
using System;
public static class Test{ public static void Show(){ Console.WriteLine(\"Hello World!\");}
}");
//编译出一个程序集
var assembly = sharpBuilder.GetAssembly();//如果你想直接获取到类型
var type = sharpBuilder.GetTypeFromShortName("Test");
type = sharpBuilder.GetTypeFromFullName("xxNamespace.xxClassName");
//同时还有
GetMethodFromShortName
GetMethodFromFullName
GetDelegateFromFullName
GetDelegateFromFullName<T>
GetDelegateFromShortName
GetDelegateFromShortName<T>//创建一个 Action 委托
//必须在同一域内,因此指定域
//写调用脚本,把刚才的程序集扔进去,这样会自动添加using引用
var action = NDelegate.UseDomain(sharpBuilder.Compiler.Domain).Action("Test.Show();", assembly);
//运行,看到 Hello World!
action();
快速 API :
NClass:
NClass builder = NClass.RandomDomain(); //使用随机域
var script = builder.CurstomeUsing() //仅使用构建过程中搜集到的using.HiddenNamespace() //不用命名空间.Access(Natasha.Reverser.Model.AccessTypes.Public) //保护级别:公有.DefinedName("EnumUT1") //类名 .Field(item=> { item.Public().DefinedName("Apple").DefinedType<int>(); }).Field(item => { item.Public().DefinedName("Orange").DefinedType<string>(); }).Property(item => { item.Public().DefinedName("Banana").DefinedType<NClass>(); }).Script;//上述 Script 结果
using System;
using Natasha.CSharp;
public class EnumUT1
{public System.Int32 Apple;public System.String Orange;public Natasha.CSharp.NClass Banana{get;set;}
}
var type = builder.GetType();
NStruct:
NStruct builder = NStruct.RandomDomain();
var script = builder.CurstomeUsing().HiddenNamespace().Attribute("[StructLayout(LayoutKind.Explicit)]").Access(AccessTypes.Public).DefinedName("XXXName").Field(item => { item.Attribute<FieldOffsetAttribute>("0").Public().DefinedName("Apple").DefinedType<int>(); }).Field(item => { item.Attribute<FieldOffsetAttribute>("0").Public().DefinedName("Orange").DefinedType<int>(); }).Script;//上述 Script 结果
using System.Runtime.InteropServices;
using System;
[StructLayout(LayoutKind.Explicit)]
public struct XXXName
{[System.Runtime.InteropServices.FieldOffsetAttribute(0)]public System.Int32 Apple;[System.Runtime.InteropServices.FieldOffsetAttribute(0)]public System.Int32 Orange;
}
var type = builder.GetType();
更多的例子详见:https://natasha.dotnetcore.xyz/
插件:
对于插件的使用如下:
// 区分流加载和文件加载
var assembly = domain.LoadPluginFromStream(path);
assembly = domain.LoadPluginFromFile(path);// 将引用从引用表中移除
domain.Remove(Assembly);
domain.Remove(path);// 后面的 assembly 一定要加上,这样保证正常的 using 引用。
var func = NDelegate.UseDomain(domain).Func<IPlugin>("return new MyPlugin();", assembly);
var iPlugin = func();
虽然,有些插件的依赖可以被动态的替换,但这里我强烈建议使用域做热更新,插件不要了就把域卸载。
五、 浅谈 Source Generators
文章地址 :https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/
对于动态构建,我分为三类:
编译前动态,比如定制代码段,IDE提示,CLI生成框架,代码生成器等等.
编译时动态,根据框架约束\IDE分析器 在编译的过程中将代码整理并注入生成结果,今儿的 Source Generators 还有 Mono.Cecils等等。
运行时动态,程序跑起来之后,根据不同的场景,动态生成所需的代码, 反射、Emit、Expression、Roslyn 及 Roslyn 之上的 Natasha。
Source Generators :源发生器?源码生成器?那不管怎么叫它,都跟源码和生成相关,目前是预览阶段,官方建议把改造的功能单独放在一个 stanadard2.0 类库工程中,资料显示,这东西是一个随着编译器和分析器一起加载的 .netstandard2.0 程序集, .NET Standard 组件能加载,它才能用。
所有的改造操作都需要在继承 ISourceGenerator 接口之后实现方法来操作,其参数中可以拿到 Compilation,对语法树集合进行一些改造,然后交由编译器再编译。
而引用它的那些工程,需要将它作为分析器添加到工程,同时需要将它作为类库引用到工程,然后写一些约定好的代码,在分析器的帮助下,它将不会报错,这样在编译阶段,将自动完成动态的构建。(上述描述若有错还请提醒~)
Roslyn 的一小步,AOT 的一大步?对于这方面的应用和封装我真的建议是再等等,再等等 = =。
六、 共同打造.NET生态
笔者还在找工作,很焦虑也很慌乱,但每次投身于 Natasha 中都会有莫名的心安,心里一直有个强烈的声音在说 Natasha 不是你个人技术生涯的终点,而是一个开源生态的起点。最近我又找到了一个新的激励自己的理由:如果你累了就想想 FreeSql 那上千个单元测试, 这阶段的难都懂,一起努力吧。
近期越来越多的开源项目涌现出来, 越来越多的好项目加入 NCC , 大家在慢慢的凝聚,廉颇不老,参与不晚,希望大家多多参与和支持 NCC 社区。
七、 鸣谢
感谢 “天天向上卡索” 对 Natasha 的一些需求建议及代码审计。
感谢 “吃着公粮” 对 Natasha 运行时反解器提供的一些技术建议及改进灵感。
感谢 FreeSql 叶老提供的需求。
感谢 WTM 团队 Vito 提供跨平台的测试信息。
感谢 Swift.Json 牛逼哥讨论的指令优化,已加入 Project。
感谢 lifecoach、Torch_zjx、cqx 等小伙伴的使用体验回馈。
本人一直认为开源项目的作者不是一个人,而是一个群体,正如 nuget 里的版权声明一样:.NET Core Community and Contributors ,后续 Authors 将根据情况进行更改。
https://github.com/dotnetcore
打赏一杯酒,削减三分愁。
跟着我们走,脱发包你有。
组织打赏账户为柠檬的账户,请标注「NCC」,并留下您的名字,以下地址可查看收支明细:https://github.com/dotnetcore/Home/blob/master/Statement-of-Income-and-Expense.md
OpenNCC,专注.NET技术的公众号
https://www.dotnetcore.xyz
微信ID:OpenNCC
长按左侧二维码关注
欢迎打赏组织
给予我们更多的支持