關於 Natasha 動態構建已經成為了封裝者們的家常便飯,從現有的開發趨勢來看,普通反射性能之低,會迫使開發者轉向EMIT/表達式樹等構建方式,但是無論是EMIT還是表達式樹,都會依賴於反射的元數據。 Natasha 通過使用 Roslyn技術,已經解決了上述的問題,在保證高效可靠的同時,提供了一 ...
關於 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