Natasha 4.0 探索之路系列(三) 基本的動態編譯

AzulX發表於2022-01-22

Natasha 的設計

動態編譯

Roslyn 為開發者提供了動態編譯的介面, 允許我們以 C# 程式碼來編寫 Emit 或 表示式樹生成的程式集, 但是完成一個編譯需要諸多步驟, 使用者參與的操作也很多, 例如: 格式化整理語法樹, 建立編譯選項, 填充對應的引用程式集來支援語義檢查和編譯, 控制輸出流等. 其中除了第一個語法樹相對簡單, 後面都需要開發者摸索完成. 畢竟 Roslyn 的文件不全, 甚至關於它的文件散落在其他邊角章節, 比七龍珠都散. 那麼在這種情況下使用 Natasha 無疑是非常好的選擇.

Natasha 的便捷之處

Natasha 自發版以來,便整合有引用管理, 全域性 Using 管理, 域管理, 這讓開發者極大的減少了開發前的準備工作, 在便捷編譯過程中, Natasha 支援引用覆蓋, Using 覆蓋,編譯流到域的輸出, 有了這三大保證, 開發者可更多的關注於動態功能邏輯的開發.
新版 Natasha 新增了語義過濾委託 API 以方便使用者根據語義資訊定製/重組自己的語法樹, 並提供方法支援開發者管理引用版本, 另外保證了3種流的對外輸出,即

  • dll : 程式集輸出檔案
  • pdb : 後設資料除錯資訊
  • xml : 後設資料結構及註釋

整個編譯過程中將會分3階段丟擲異常:

  1. 語法構建階段,如果出錯則丟擲異常;
  2. 編譯階段, 如果編譯失敗則會丟擲異常;
  3. 後設資料轉換階段, 有些 API 是支援從 Assembly 到其他後設資料獲取和轉換的, 轉換失敗則丟擲異常.

Natasha 基本編譯單元

Natasha 的基本編譯單元為 AssemblyCSharpBuilder , 該單元整合了編譯流程所需要的基本功能, 相比 Natasha 的模板而言, 它則是輕量級,底層的工作單元.
以下是使用方法:

首先引入 DotNetCore.Natasha.CSharp

最基本的編譯操作

//Natasha 預熱
NatashaInitializer.Preheating(/*...引用新增過濾器...例如:(item, name) => name!.Contains("IO")*/);

string code = @"public class A{public string Name=""HelloWorld"";}";

//在花括號範圍內圈定域,using 內的方法鎖定了域的作用範圍.
//Natasha 所有關於 Name 的 Api 如果不指定,預設為 GUID.
using (DomainManagement.Create(domainName)/Random().CreateScope())
{
  AssemblyCSharpBuilder builder = new( /*....assenblyName....*/ );
  builder.Add(code);
  var type = builder.GetTypeFromShortName("A");  
  //...do sth...        
}


//手動指定域
AssemblyCSharpBuilder builder = new();
builder.Domain = DomainManagement.Random();
builder.Add(code);
var assembly = builder.GetAssembly();  
//...do sth...        


//直接定位到委託
string code = @"public class A{public string Name=""HelloWorld""; public static string Get(){  return (new A()).Name; }}";
using (DomainManagement.Create("myDomain").CreateScope())
{
   AssemblyCSharpBuilder builder = new("myAssembly");
   builder.Add(code);
   var func = builder.GetDelegateFromShortName<Func<string>>("A","Get");
   Assert.Equal("HelloWorld", func()); // √
}
其他 API
//設定輸出 dll 檔案路徑
builder.SetDllFilePath(mydll);
//設定輸出 pdb 檔案路徑
builder.SetPdbFilePath(mypdb);
//設定輸出 xml 檔案路徑
builder.SetXmlFilePath(myxml);
//使用 Natasha 自帶的輸出路徑(請在域和程式集名確定之後呼叫).
builder.UseNatashaFileOut();

//配置編譯選項
builder.ConfigCompilerOption(opt=>opt);
//配置語法樹選項
builder.ConfigSyntaxOptions(opt=>opt);

//給編譯單元新增語義過濾
builder.AddSemanticAnalysistor();
//啟/禁用語義過濾
builder.Enable/DisableSemanticCheck();


//新增日誌事件
builder.LogCompilationEvent += (log) => { if(log.HasError) Console.WriteLine(log.ToString()); };

//編譯事件
builder.CompileSucceedEvent //編譯成功觸發事件
builder.CompileFailedEvent //編譯失敗觸發事件


//引用行為與程式集載入行為控制
var assembly = builder

     //委託過濾: 如果發現預設域的引用與定製域中的引用有同名情況,則進入委託處理. 返回一個列舉結果給程式處理.
     //PassToNextHandler 結果表示將進入到引用版本行為控制繼續處理
    .CompileWithReferencesFilter((defaultAssemblyName,domainAssemblyName)=> LoadVersionResultEnum.PassToNextHandler)

     //引用行為控制, None/UseHighVersion/UseLowVersion/UseDefault(預設使用)/UseCustom 四種控制方法
    .CompileWithReferenceLoadBehavior(referenceLoadBehavior)

     //程式集編譯成功後,在域中載入的行為控制,預設為 LoadBehaviorEnum.None (全載入);
    .CompileWithAssemblyLoadBehavior(LoadBehaviorEnum.UseDefault)

    .GetAssembly();

注意: 主域的引用檔案和自己建立域的引用檔案可能存在同名,但不同版本,此時編譯需要 CompileWithReferenceLoadBehavior 來控制引用載入的行為, 舉例: RefA(v1.0) 和 RefA(v2.0) 相比, v2.0 中比 v1.0 多了幾個功能,幾個類,幾個介面.... 那麼在管理引用的時候, 你就要根據自身的程式碼情況進行管理, 比如你的程式碼用到了 v2.0 的新類,新功能, 那麼就要遮蔽掉 v1.0.

覆蓋全域性 using
//-------------------主域 using -------------------- 定製域 using ------------------------------- 程式碼指令碼 ---------------
string code = DefaultUsing.UsingScript +  builder.Domain.UsingRecorder.ToString() + "namespace{ public class xx.....  }";

域中的 UsingRecorder 會記錄編譯之後產生的 using, 自動管理.

結尾

大家在使用動態編譯時, 要儘可能做到"隔離", 一旦依賴和引用版本多了, 對於動態開發來講,就是一場災難.
以上是使用 Natasha 關於動態編譯的最基本使用方法, 下一篇將講解 Natasha 高階 API 的使用.

相關文章