SourceGenerator的應用: .Net多程式開發庫 - Juxtapose

internalnet發表於2022-01-25

背景

程式間通訊屬於老生常談的話題,可能已經有很多的通訊示例程式碼,但在實際使用中需要做的東西還比較多。例如協議定製、訊息收發、程式管理等都需要實現,進階需求可能還需要實現回撥函式、取消等。
個人在工作中恰好遇到了相關需求,然後在網上搜了一下,只找到兩個.net下多程式相關的庫(有可能是搜尋方式不對)
https://github.com/tmds/Tmds.ExecFunction
https://github.com/CyAScott/AppDomainAlternative/
但並不滿足需求,不能程式複用,或者不能方便的進行回撥、取消。於是乎進行手寫相關程式碼。
在手寫了兩三個相似的專案後,覺悟到重複的勞動是可以進一步省略的,於是進行了封裝,產生了該專案 - Juxtapose 基於 SourceGenerator 的硬編碼 .Net程式執行庫。只需要幾行額外的程式碼就能使現有程式碼(*)支援多程式執行,程式複用,支援LinuxWindows,支援回撥委託CancellationToken,支援除錯子程式(*)。無顯式的執行時反射呼叫和動態構造類。
好處很多,那麼代價是什麼呢?

  • 簡單的一次方法呼叫將會經歷資料傳輸以及至少四次序列化或反序列化;對比程式內方法呼叫來說是巨大的損耗。
  • 方法引數只能是簡單的資料型別(委託和CancellationToken除外,它們進行了特化處理),不能傳遞有行為的物件。
  • 跨程式的異常資訊將被包裹,需要自行解析;
  • 呼叫堆疊將會變複雜;

綜上,使用時需要能夠接受和處理上述情況。

Juxtapose早期版本已經在生產環境進行了長時間的使用驗證,但新的Release版本暫時還沒有;

相關技術

SourceGenerator

源生成器 - SourceGenerator 已經推出有一段時間了,參見官方公告 Introducing C# Source Generators
官方簡介:
源生成器是一項 C# 編譯器功能,使 C# 開發人員能夠在編譯使用者程式碼時進行檢查,並動態生成新的 C# 原始檔,以新增到使用者的編譯中。 通過這種方式,你的程式碼可以在編譯過程中執行並檢查你的程式以生成與其餘程式碼一起編譯的其他原始檔。
使用SourceGenerator可以在編譯時生成程式碼,避免執行時反射,提高執行效率,使FullAOT成為可能,程式碼執行邏輯也更加直觀。不在此處進行過多描述,已經有不少的文章對其進行了介紹,可以隨便搜尋到,官方也有完善的示例程式碼。

相關資源:

程式間通訊

通訊方式很多,可以參考文章 c#多程式通訊,今天,它來了
Juxtapose預設實現了命名管道通訊(有需求可以自行實現其他方式進行替換),預設使用命名管道的主要原因是它由.net原生整合、支援雙工通訊、跨平臺。

使用場景

以下為實際使用場景,更多場景請自行結合實際需求。

  • 某元件不允許在一個程式內建立多個例項,無法併發;
  • 某元件使用了過多記憶體、記憶體洩漏、記憶體釋放不及時,導致程式被殺、Pod重啟、服務中斷;
  • 某元件執行某耗時操作時,不支援取消操作,且佔用大量CPU/記憶體資源,影響程式執行;
  • 某元件直接操作記憶體,會偶發性出現非法訪問,導致程式退出;

Nuget包介紹

1. Intro

基於 SourceGenerator 的硬編碼 .Net程式執行庫。

包列表

名稱 介紹
Juxtapose 執行時庫,封裝通用的功能
Juxtapose.SourceGenerator 原始碼生成器,用於程式碼生成
Juxtapose.VsDebugger VisualStudio除錯附加包,用於子程式除錯

2. Features

  • 可以為型別介面靜態類生成代理,無需手動編寫RPC相關程式碼,即可多程式執行;
  • 編譯時生成所有程式碼,執行時無顯式的反射呼叫和動態構造;
  • 支援委託CancellationToken型別的方法引數(其餘型別未特殊處理,將會進行序列化,目前回撥委託不支援巢狀和CancellationToken);
  • 支援LinuxWindows(其它未測試);
  • 支援除錯子程式(Windows&&VisualStudio Only);

注意事項

  • 目前引數不支援定義為父型別,實際傳遞子型別,序列化時將會按照定義的型別進行序列化和反序列化,會導致具體型別丟失;
  • 目前所有的引數都不應該在方法完成後進行保留,CancellationToken委託等在方法完成後會被釋放;

3. Requirement

  • .Net5.0+(其它版本沒有嘗試過)

4. 使用方法

4.1 引用包

<ItemGroup>
  <PackageReference Include="Juxtapose" Version="1.0.0" />
  <PackageReference Include="Juxtapose.SourceGenerator" Version="1.0.0" />
</ItemGroup>

4.2 建立上下文

4.2.1 建立上下文型別,並使用 [Illusion] 特性指定要生成的型別

[Illusion(typeof(Greeter), typeof(IGreeter), "Juxtapose.Test.GreeterAsIGreeterIllusion")]
public partial class GreeterJuxtaposeContext : JuxtaposeContext
{
}

示例程式碼將為Greeter生成IGreeter介面的代理型別Juxtapose.Test.GreeterAsIGreeterIllusion

Note!!!

  • 必須繼承JuxtaposeContext
  • 必須標記partial關鍵字;

4.2.2 [Illusion] 的多種用法

  • 直接為型別生成代理,如下示例生成 Juxtapose.Test.GreeterIllusion 型別,且不繼承介面(靜態型別相同用法)
[Illusion(typeof(Greeter))]
  • 為型別生成代理,並繼承指定介面,如下示例生成 Juxtapose.Test.GreeterAsIGreeterIllusion 型別且繼承IGreeter介面
[Illusion(typeof(Greeter), typeof(IGreeter))]
  • 生成型別,並指定型別名稱,如下示例生成 Juxtapose.Test.HelloGreeter 型別
[Illusion(typeof(Greeter), generatedTypeName: "Juxtapose.Test.HelloGreeter")]
  • 生成從IoC容器獲取的介面代理型別,如下示例生成 Juxtapose.Test.IGreeterIllusionFromIoCContainer 型別(此時Context類需要實現IIoCContainerProvider介面,並提供有效的IServiceProvider
[Illusion(typeof(IGreeterFromServiceProvider), generatedTypeName: "Juxtapose.Test.IGreeterIllusionFromIoCContainer", fromIoCContainer: true)]

4.3 新增入口點

Main方法開始處新增入口點程式碼,並使用指定上下文

await JuxtaposeEntryPoint.TryAsEndpointAsync(args, GreeterJuxtaposeContext.SharedInstance);

到此已完成開發,建立型別Juxtapose.Test.GreeterAsIGreeterIllusion的物件,並呼叫其方法,其實際邏輯將在子程式中執行;

5. 除錯子程式(Windows&&VisualStudio Only)

5.1 引用除錯包

在Host專案檔案中新增包引用

<ItemGroup Condition="'$(Configuration)' == 'Debug'">
  <PackageReference Include="Juxtapose.VsDebugger" Version="1.0.0" />
</ItemGroup>

建議和示例程式碼一樣,新增條件引用,只在Debug環境下引用除錯包

5.2 新增入口點

Main方法開始處新增除錯入口點程式碼

JuxtaposeDebuggerAttacher.TryAttachToParent(args);

在程式碼中打上斷點,執行時將會正確命中斷點(只在 VisualStudio2022 17.0.5 && Win11 21TH2 中進行了測試,理論上是通用)

6. 工作邏輯

SourceGenerator在編譯時生成代理型別,封裝通訊訊息。在建立代理型別物件時,會自動建立子程式,並在子程式中建立目標型別的物件,使用命名管道進行程式間通訊,使用System.Text.Json進行訊息的序列化與反序列化。更多技術細節可以參考原始碼。

連結

原始碼: https://github.com/stratosblue/juxtapose
Nuget: https://www.nuget.org/packages/Juxtapose

相關文章