Natasha v9.0 為 .NET 開發者提供 [熱執行] 方案.

AzulX發表於2024-12-03

專案簡介

自 Natasha v9.0 釋出起,我將基於 Natasha 的推出熱執行方案,這項技術允許基於 控制檯(Console) 和新版 Asp.net Core 架構的專案在執行中動態重編譯,在不停止工程的情況下獲取最新結果,以幫助技術初學者、專案初期開發人員等,進行快速實驗以及試錯。

為了更形象的說明 [熱執行] 請看下圖:
HE

熱執行

以下為了更加簡潔,稱熱執行為 [HE].
圖中是 Asp.net Core 一個介面開發的案例,我更改了一個實體類的結構,並儲存,可以看到介面返回了最新的實體類結構。藉此簡單闡述一些熱執行的工作原理,檔案發生變化會觸發 [HE] 對專案進行熱編譯,開發者無論是大改還是小改,只要你的專案檔案(cs) 、依賴專案、csproj 發生了變化,[HE] 就會代理整個專案並自動編譯輸出。對於有些老機器較慢,可能 [HE] 熱編譯要比 [按下F5-程式跑起來] 要快的多。

熱過載與熱執行

也許有人會覺得這更像一個完全體的熱過載,並不是,這是與熱過載完全不同的技術,[HE] 的核心技術是語法樹重寫與動態編譯。而熱過載是對 Runtime 的程式集進行熱更新,熱過載嚴重依賴 Debugger 元件,且目前從 ENC 錯誤程式碼 來看這項技術的限制還是很大的。
起初我也是悶頭鑽研熱過載技術,但實驗效果很不理想,熱過載技術是一項前沿的,邊界明確的技術,並不適合敞開手腳快刀闊斧的幹,由於不是面對開發者,(截至2024年8月)資料也不是很多。與其死磕它,不如另闢蹊徑,藉助 Natasha 動態代理將專案管理起來。

指令簡介

註釋指令

HE 使用註釋作為熱代理指令,這些指令會影響語法樹重建以及熱編譯選項,但不影響程式的釋出和使用。目前具體如下:

  • 最佳化級別

使用 //HE:Release 指令允許在 HE 重編譯時,使用 Release 模式進行編譯。

  • 非同步代理

當 Main 方法中有物件 A, A 需要延遲解除安裝,A 不干擾 new A (即全域性可以不只有一個 A), 此時使用 //HE:Async 允許 HE代理 在上一次 A 物件未完全銷燬時非同步執行新 Main 方法。

  • Using 排除

由於開發可能會開啟隱式 using, 若開啟,則 HE 在代理期間,會載入所有記憶體中存在的名稱空間,因此有機率會出現 using 二義性引用問題,使用 //HE:CS0104 可以排除干擾 using,例如 //HE:CS0104 using1;using2...

  • 動態表示式

如果您需要在 HE 代理期間動態的除錯輸出一些結果,且不影響程式釋出,您可以使用 //DS 或 //RS 指令輸出其後的表示式。例如 //DS 1+1 在 Debug 模式下輸出 2. //RS a.age+b.age 在 Release 輸出兩個物件年齡相加。

  • 引數傳遞

void ProxyMainArguments() 方法將在代理執行之前執行,該方法允許開發者在動態開發中,在 HE 代理期間模擬 控制檯向 main 方法中傳遞引數。
虛擬碼類似於:

public static void ProxyMainArguments()
{
      HEProxy.AppendArgs("123");
      HEProxy.AppendArgs("引數2");
      HEProxy.AppendArgs("abc");
}
main("123","引數2","abc");

注意:HE 每次建立新的代理都是一次全新的 main 執行過程,因此將清空 Args, 避免上一次代理干擾本次執行。

  • 僅在程式第一次執行

使用 //Once 命令允許程式僅在程式第一次開啟時執行被其註釋的程式碼,在後續的 HE 代理期間,被註釋的語法節點將被剔除。

使用

目前該專案支援 .NET3.0 即以上版本,且 .NET5.0 版本以上有 Source Generator 技術加持。

無 SG 加持的版本

  1. 引入熱執行包:DotNetCore.Natasha.CSharp.HotExecutor
class Program
{

        public static void Main(string[] args)
        {

            //設定當前程式的型別 ,預設為 Console
            HEProxy.SetProjectKind(HEProjectKind.Console);

            //HE 代理週期日誌(如果不需要 HE 寫入日誌,這句就不用寫了)
            string debugFilePath = Path.Combine(VSCSProjectInfoHelper.HEOutputPath, "Debug.txt");
            HEFileLogger logger = new HEFileLogger(debugFilePath);

            //設定資訊輸出方式,該方法影響 DS/RS 指令的輸出方式
            //預設是 Console.WriteLine 方式輸出
            HEProxy.ShowMessage = async msg => {
                //一些專案可能禁用控制檯,那就用日誌輸出 HE 資訊
                await logger.WriteUtf8FileAsync(msg);
            };

            //編譯初始化選項,主要是 Natasha 的初始化操作.
            //Once (熱編譯時使用 Once 剔除被註釋的語句)
            HEProxy.SetCompileInitAction(() => {
                {
                    NatashaManagement.RegistDomainCreator<NatashaDomainCreator>();
                    NatashaManagement.Preheating((asmName, @namespace) =>

                                !string.IsNullOrWhiteSpace(@namespace) &&
                                (HEProxy.IsExcluded(@namespace)),
                                true,
                                true);
                }
            });

            //開始執行動態代理.
            //Once (熱編譯時使用 Once 剔除被註釋的語句)
            HEProxy.Run();

            for (int i = 0; i < args.Length; i++)
            {
                Console.WriteLine(args[i]);
                //在 HE 代理期間輸出 args 值
                //DS args[i]
            }
 
            //while 阻塞時需要指定 CancelToken ,熱執行時 HE 將取消迴圈操作。
            CancellationTokenSource source = new CancellationTokenSource();

            //新增到 HE 中,以便下個編譯時釋放
            source.ToHotExecutor();

            while (!source.IsCancellationRequested)
            {
                Thread.Sleep(1000);
                //在 HE 代理期間輸出 "In while loop!"
                //DS "In while loop!"
            }
  
            for (int i = 0; i < args.Length; i++)
            {
                 Console.WriteLine(args[i]);
            }
  
            //防止 while 退出後直接關閉主執行緒
            //Once (這句 `//Once` 可以不寫,HE 有針對 “Console.Read” 的末尾阻塞檢測)
            Console.ReadKey();

        }

        //方法體中的引數操作對應 Main(string[] args) 中的 args,  
        //熱執行時,Main 將接收到 "引數11",“引數2”,“引數23”
        //非必要,可以不寫
        public static void ProxyMainArguments()
        {
            HEProxy.AppendArgs("引數11");
            HEProxy.AppendArgs("引數2");
            HEProxy.AppendArgs("引數23");
        }
}

這段程式碼是 HE 最原始的程式碼。

SG 加持(.NET5.0及以上版本)

SG 主要是減少了 HE 初始化的一些操作。而一些需要手動傳遞的 cancel/dispose 例項仍然需要手動傳遞給 HE。

簡單案例

  1. 引入 SG 包:DotNetCore.Natasha.CSharp.HotExecutor.Wrapper
internal class Program
{

    static void Main(string[] args)
    {
        for (int i = 0; i < args.Length; i++)
        {
            //DS args[i]
        }

        //這裡仍然需要手動將 canceltoken 傳遞給 HE
        CancellationTokenSource source = new();
        source.ToHotExecutor();

        while (!source.IsCancellationRequested)
        {
            Thread.Sleep(1000);
            //DS "In while loop!"
        }
     
        //防止 while 退出後直接關閉主執行緒
        Console.ReadKey();
    }
    public static void ProxyMainArguments()
    {
        HEProxy.AppendArgs("引數1");
        HEProxy.AppendArgs("引數2");
        HEProxy.AppendArgs("引數3");
        HEProxy.AppendArgs("引數4");
    }
}

代理新 Asp.net Core

HE 目前不能代理 MVC 專案和老版的 API 專案。

public class Program
{
    public static void Main(string[] args)
    {

        //HE:Async
        var builder = WebApplication.CreateBuilder(args);
        builder.Services.AddAuthorization();
        builder.Services.AddEndpointsApiExplorer();
        builder.Services.AddSwaggerGen();
        var app = builder.Build();
        if (app.Environment.IsDevelopment())
        {
            app.UseSwagger();
            app.UseSwaggerUI();
        }


        //將 APP 新增到 HE 中,以便在下一次編譯中釋放該物件。
        app.AsyncToHotExecutor();

        //更改以下的值,儲存檔案,會觸發 HE 建立新的 WebApplicationBuilder
        var summaries = new[]
        {
            "Freezing441", "Bracing"
        };


        app.MapGet("/weatherforecast", (HttpContext httpContext) =>
        {
            var forecast = Enumerable.Range(1, 5).Select(index =>
                new WeatherForecast
                {
                    Date = DateTime.Now.AddDays(index),
                    TemperatureC = Random.Shared.Next(-20, 55),
                    Summary = summaries[Random.Shared.Next(summaries.Length)]
                })
                .ToArray();
            return forecast;
        })
        .WithName("GetWeatherForecast");

        app.Run();
    }
}

其他專案支援

截至目前而言, HE 對 Winform 的支援不是很好,WPF 的很難代理,時間和精力有限,不會深入去研究了。

鳴謝

感謝 九哥 的支援。

結尾

遇到問題可以到 Natasha Issue 區 提出反饋。

相關文章