兼容 .NET Core3.0, Natasha 框架实现 隔离域与热编译操作

2022-10-16,,,,

关于 natasha

   动态构建已经成为了封装者们的家常便饭,从现有的开发趋势来看,普通反射性能之低,会迫使开发者转向emit/表达式树等构建方式,但是无论是emit还是表达式树,都会依赖于反射的元数据。
natasha 通过使用 roslyn技术,已经解决了上述的问题,在保证高效可靠的同时,提供了一条相对完整的动态编译链,以c#语法轻松构建动态代码,学习成本很低,排查以及维护方面有正确友好的异常输出。为此以roslyn相关模块功能为基础,封装了natasha, natasha使用友好的api和层级分明的模板,极大的提升了开发者构建动态代码的体验,让事情变得更简单,人人都可以低成本构建动态代码,人人都可以定制自己喜爱的动态功能。

文章内容未经许可,禁止转载!
natasha 属于 ncc(.net core community) 成员项目。
项目仓库:https://github.com/dotnetcore/natasha

一、 2.0预览版本增加了哪些功能

大部分为底层的升级优化,例如:

  • 引擎兼容 core3.0

    • 优化编译流程,增加编译前语法检测及日志,统一采用流加载方式

    • 在 vito 的建议下改进了日志目录及命名

    • alc 同类覆盖编译

    • 支持域的创建、卸载、锁操作

    • 支持共享域与独立域协作

    • 支持独立域的程序集创建、覆盖操作

    • 支持插件及依赖的加载

  • 构建方面的强化,例如:

    • 支持枚举的构建和编译

    • 在 vito 的建议下增加了多维数组反解器

    • 在 vito 的建议下增加了锯齿数组反解器

    • 命名反解器支持锯齿和多维数组

二、我们经历了哪些实践

  • 深度克隆:https://github.com/night-moon-studio/deepclone

本项目由 net_win、vito、myfirstway、白开水组队开发,可在运行时动态生成克隆方法。深度克隆作为基础项目,锻炼了开源工作者的类型辨识技能,趟过了坑为以后的封装之路打下基础。

  • 快速调用:https://github.com/night-moon-studio/ncaller

本项目由 azulx 和 future* 开发,可以对运行时实体类、静态类的字段/属性进行动态调用和赋值,目前有两个主要分支,哈希二叉查找算法动态实现以及 future* 的指针二叉查找算法动态实现,在算法的动态实现上,natasha 表现出了相当强大的优势。

三、谈一谈‘热更新’

'热更新'是 core3.0 的亮点特性之一,不少小伙伴在看到译文的时候可能就已经想到了n多场景,历经两代 .net 的洗礼,‘热更新’现在发展到什么样子了?下面简单谈一谈:

.net framework 开荒时期有 appdomain 域之隔离术,包括有创建、加载程序集、卸载等方法,囊括百家程序集,一刀以斩之。对于前辈们来说谈到 appdomain 可以口若悬河滔滔不绝,可惜我进入 c# 时间比较晚,对 appdomain 的印象并不是很深,在应用上也没有什么造诣,仅此泛泛而言。

时间进入了 .netcore 时代,appdomain 在升级大潮中受到了致命打击, create 方法和 unload 方法经岁月升级后的源码中充斥着 throw 和 throw ,完全丧失了功能,取而代之的是 alc(assemblyloadcontext) ,core3.0 的 alc 是一个更为完善的操作类,官方为其定义了三大洪荒场景:

1、插件编程

2、动态编译,运行/刷新代码,网站/脚本引擎

3、外部程序集的一次性内省(我个人理解就是类的信息,isarray ,  isclass 这种元数据只读属性)  

据描述:roslyn 之前一直用 appdomain , 每个测试都腰酸背痛相当慢,自从换了 alc( a blue ca.) 一口气上5楼不费劲!官方画了大饼:未来 roslyn 分析器执行编译时也都在alc里进行,用完就卸载,卸磨就杀驴。

appdomain 当初被定位在高性能、安全,历史证明这个定位跟 gps 一样不准,asp.net 深受其害,历史车轮碾过了 asp.net 迎来了 asp.net core ,在域功能被阉割的期间,asp.net core 转向了相对静态的模型,增加了若干学习成本,详见 dotnet watch 命令。还有 razor , 它从 .cshtml 编译到 .dll 的环境就是 alc ,自建了一个名为 razor-server 的域环境。

另外还涉及到 linqpad 和 prism 框架, 精力有限,谁有兴趣就去研究研究吧。

alc 的场景和案例可能激起了您的好奇心,下面讲一下 alc 的应用:

我们可以在程序里创建多个 alc 实例,但前提是你需要继承并实现它。每一个 alc 的实例都是一个域(这里我就不叫它上下文了)。程序刚跑起来的时候是在 defualt 域中的,这个域属于系统域卸不了,又称为共享域,不同域之间是无法访问和引用的不同域中信息的,却共用 default 域中的信息,这个域至关重要,所以尽量避免向其中加载乱七八糟的程序集。

alc 的使用需要注意以下几点:

1、子类继承时需指定 alc 的构造参数,base(iscollectible) , 这个参数可以赋予 alc 卸载的能力。



2、时刻注意反射信息的引用,只有清除引用,才能保证 alc 实例被 gc 回收。



3、在针对不同域的编程时可使用 entercontextualreflection 方法锁住域内上下文,entercontextualreflection 方法是放在 using 里的,这样你的花括号内就是一个域,并用 currentcontextualreflectioncontext 属性来获取当前操作域。



4、注意 alc 被线程占用的情况,被占用的对象是无法被回收的,如果你在测试中没有达到预期,除了排除代码问题之外你还需要注意函数是否被内联进入主线程或一个带有阻塞功能的线程,如果你不确定,可以在方法上使用 [methodimpl(methodimploptions.noinlining)] 阻止代码内联优化,正常情况下优化功能是开启的 。



5、插件加载要注意与插件 dll 同目录的依赖文件,3.0 提供了 assemblydependencyresolver 操作类自动解析依赖,建议使用带有.deps.json文件的完整插件。



6、当你的外部文件引用并使用了 json.net/sqlconnection 等(测试日期9月3日),会造成不可回收的情况,不是你的代码出问题了,而是库本身的问题(待解决,3.1或者5.0)。  

对 alc 封装的一些建议:

1、如果没有非托管代码,尽量不要在析构函数里折腾代码。



2、如果你的域管理代码有些复杂,建议对外给个 idispose 接口,以便清除对该域的程序集、元数据等信息的引用。



3、肉眼观测内存时,测试代码中尽量不要在 main 函数里做元数据的相关操作,主线程是 gc 的一个干扰点。



4、若对内存的开销比较敏感,请尽可能分域,并结合弱引用实现创建与销毁。



5、有时显式调用 unload 方法会报异常,可以在 dispose 里清除完引用之后再使用,实测你不用 unload 方法也能回收。  

core3.0 中随 alc 一起的还有反射的自省信息。

例如:memberinfo.iscollectible 、 assembly.iscollectible 等元数据,它将告诉你它是否能被回收,当然了这种自省的信息都是只读的。说到只读,.net 中还存有一条进化路线即 :reflectiononlyload -> typeloader -> metadataloadcontext (感谢weihanli提供的信息), 只读元数据,相比 alc 可执行,可调用,mlc ( metadataloadcontext 在包 system.reflection.metadataloadcontext 中) 关注的是元数据只读操作,它并不能执行程序集的内容,仅仅反射出元数据,配套使用的是pathassemblyresolver.

对于无法卸载的情况,官方建议使用 windbg sos 组件进行调试,新版 sos 将独立出来,各位可以使用以下命令进行安装(建议开源工作者在封装此功能时添加ut测试检测卸载功能,尽可能保证在正常的情况下不需要用户自己去调试)。

$ dotnet tool install -g dotnet-sos --version 3.0.0-preview8.19412.1
$ dotnet-sos install
更多的实践还需要大家去探索。

四、natasha是如何实现‘热更新’的

  • 关于域的操作您可以
//创建一个域
domainmanagment.create("mydomain");
//移除一个域,移除将无法进行domainmanagment的其他任何操作
domainmanagment.remove("mydomain");
//判断域是否被卸载(被gc回收)
domainmanagment.isdeleted("mydomain");
//获取一个alc上下文
domainmanagment.get("mydomain");


//锁住已存在的域上下文
using(domainmanagment.lock("mydomain"))
{
    var domain = domainmanagment.currentdomain;
    //code in 'mydomain' domain 
}
//创建并锁定一个域上下文
using(domainmanagment.createandlock("mydomain"))
{
    var domain = domainmanagment.currentdomain;
    //code in 'mydomain' domain 
}
  • 关于程序域的插件操作
//向域中注入插件 
string dllpath = @"1/2/3.dll";
var domain = domainmanagment.get/create("mydomain");
var assembly = domain.loadfile(dllpath);


//锁域与插件解构操作
string dllpath = @"1/2/3.dll";
using(domainmanagment.createandlock("mydomain"))
{
    var (assembly,typecache) = dllpath;
    //assembly: assembly
    //typecache: concurrentdictionary<string,type> 
}


//将引用从当前域内移除,下次编译将不会带着该程序集的信息
//下面方法三选一均可实现引用移除操作
domain.removedll(dllpath);
domain.removeassembly(assembly);
domain.removetype(type);
  • 关于程序集的操作
//从指定域创建一个程序集操作实例
var asm = domain.createassembly("myassembly");


//向程序集中添加一段已经写好的类/结构体/接口/枚举
asm.addscript(@"using xxx; namespace xxx{xxxx}");
asm.addfile(@"class1.cs");


//使用natasha内置的操作类
asm.createenum(name=null);
asm.createclass(name=null);
asm.createstruct(name=null);
asm.createinterface(name=null);


//使用natasha内置的方法操作类
//并不是很推荐使用这两个方法
//建议在一个单独的程序集内编译方法 
asm.createfastmethod(name=null);
asm.createfakemethod(name=null);


//使用程序集进行编译并获得程序集
var assembly = asm.complier();
asm.gettype(name);
  • 结合域和程序集动态编译,实例
using(domainmanagment.createandlock("mydomain"))
{
    var domain = domainmanagment.currentdomain;
    var assembly = domain.createassembly("myassembly");
    
    
    //创建一个接口
    assembly
        .createinterface("interfacetest")
        .using("system")
        .oopaccess(accesstypes.public)
        .oopbody("string showmethod(string str);");
        
        
     //创建一个类并实现接口
     assembly
        .createclass("testclass")
        .using("system")
        .oopaccess(accesstypes.public)        
        .inheritance("interfacetest")
        .method(method => method
          .memberaccess(accesstypes.public)
          .name("showmethod")
          .param<string>("str")
          .body("return str+\" world!\";")
          .return<string>());
          
          
      //编译并获取类型
      var result = assembly.complier();
      var type = assembly.gettype("testclass");
      
      
      //operator默认单独创建一个程序集
      var @delegate = fastmethodoperator.new
        .using(type)
        .methodbody(@"
            testclass obj = new testclass();
            return obj.showmethod(arg);")
        .complie<func<string, string>>();
       
       
       @delegate("hello");  //result = "hello world!";
       domain.dispose();    //卸磨杀驴
}

文章内容未经许可,禁止转载!
natasha 属于 ncc(.net core community) 成员项目。
项目仓库:https://github.com/dotnetcore/natasha

《兼容 .NET Core3.0, Natasha 框架实现 隔离域与热编译操作.doc》

下载本文的Word格式文档,以方便收藏与打印。