1. 程式人生 > >SourceGenerator入門指北

SourceGenerator入門指北

### 1 SourceGenerator介紹 SourceGenerator於2020年4月29日在微軟的[.net blog](https://devblogs.microsoft.com/dotnet/introducing-c-source-generators/)首次介紹,大概說的是開發者編可以寫分析器,在專案程式碼編譯時,分析器分析專案既有的靜態程式碼,允許新增原始碼到GeneratorExecutionContext中,一同與既有的程式碼參與編譯。 ### 2 SourceGenerator未出生時 在還沒有SourceGenerator的時候,開發者要實現AOP框架時,往往使用以下技術: * Emit技術,執行時生成代理型別,難點比較低且不用考慮語言的語法,但不適用於需要完全AOT編譯的平臺。 * msbulid+程式碼分析+程式碼生成,攔截build的某個階段執行task,task分析既有程式碼的語法,然後生成代理程式碼到編譯器中。 * msbuild+Mono.Cecil, 攔截build的某個階段執行task,task通過Cecil靜態修改編譯輸出的程式集,補充代理IL到程式集中,然後程式集可能會繼續參與下一步的AOT編譯過程。 WebApiClient.JIT與WebApiClient.AOT包,分別適用上面的Emit和Cecil,後者難度非常大,且表現得不太穩定。 ### 3 第一個吃螃蟹的落地專案 一直比較關心SourceGenerator,現在我覺得,SourceGenerator現在已到達可以使用的階段了。[WebApiClientCore](https://github.com/dotnetcore/WebApiClient)之前有個分支做SourceGenerator的實驗,但遲遲沒有合併到master來。現在它已經合併到master,並以一個[Extensions.SourceGenerator擴充套件包](https://github.com/dotnetcore/WebApiClient/tree/master/WebApiClientCore.Extensions.SourceGenerator)的方式出現,讓[WebApiClientCore](https://github.com/dotnetcore/WebApiClient)多一種代理類生成的方式選擇。這個擴充套件包編寫時非常簡單,我已經不想看以前是怎麼用Cecil為程式集插入靜態IL的程式碼了。 ### 4 如何編寫xxxSourceGenerator #### 建立一個netstandard2.0的程式集 ``` netstandard2.0 8.0 enable ``` #### 實現ISyntaxReceiver,接收編譯時語法樹的遍歷 ``` class xxxSyntaxReceiver : ISyntaxReceiver { /// /// xxx感興趣的介面列表 /// private readonly List interfaceSyntaxList = new List(); /// /// 訪問語法樹 /// /// void ISyntaxReceiver.OnVisitSyntaxNode(SyntaxNode syntaxNode) { if (syntaxNode is InterfaceDeclarationSyntax syntax) { this.interfaceSyntaxList.Add(syntax); } } } ``` #### 實現ISourceGenerator,且使用[Generator]特性 ``` [Generator] public class xxxSourceGenerator : ISourceGenerator { /// /// 初始化 ///
/// public void Initialize(GeneratorInitializationContext context) { context.RegisterForSyntaxNotifications(() => new xxxSyntaxReceiver()); } /// /// 執行 /// /// public void Execute(GeneratorExecutionContext context) { if (context.SyntaxReceiver is xxxSyntaxReceiver receiver) { // 從receiver獲取你感興趣的語法節點 // 然後拼接成string的程式碼 // 把程式碼新增到context context.AddSource("程式碼1的id","這裡是c#程式碼,會參與編譯的"); } } } ``` ### 5 如何除錯xxxSourceGenerator #### 在被除錯專案以分析器方式引入xxxSourceGenerator專案 ```
``` #### 在xxxSourceGenerator里加入Debugger.Launch() 沒錯,這是最簡單的觸發除錯方式,你在xxxSourceGenerator入口加這麼一行程式碼,被除錯的專案只要一編譯,vs就彈出且斷點到Debugger.Launch()這行,然後就可以一步一步執行除錯了。 ### 6 如何打包釋出xxxSourceGenerator SourceGenerator專案本質上還是分析器專案,所以可以打包成一個nuget包,別的專案引用這個nuget包之後,就自動以分析器的方式安裝到目標專案中,然後激活了你的xxxSourceGenerator。 #### 分析器的nuget打包 * 需要將編譯出的xxxSourceGenerator.dll放到nuget包的analyzers\dotnet\cs目錄下 * 需要在nuget包的tools目錄下放置分析器安裝和解除安裝指令碼install.ps1和uninstall.ps1,這指令碼是通用的。 #### install.ps1 ``` param($installPath, $toolsPath, $package, $project) $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers" ) * -Resolve foreach($analyzersPath in $analyzersPaths) { # Install the language agnostic analyzers. if (Test-Path $analyzersPath) { foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll) { if($project.Object.AnalyzerReferences) { $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) } } } } # $project.Type gives the language name like (C# or VB.NET) $languageFolder = "" if($project.Type -eq "C#") { $languageFolder = "cs" } if($project.Type -eq "VB.NET") { $languageFolder = "vb" } if($languageFolder -eq "") { return } foreach($analyzersPath in $analyzersPaths) { # Install language specific analyzers. $languageAnalyzersPath = join-path $analyzersPath $languageFolder if (Test-Path $languageAnalyzersPath) { foreach ($analyzerFilePath in Get-ChildItem $languageAnalyzersPath -Filter *.dll) { if($project.Object.AnalyzerReferences) { $project.Object.AnalyzerReferences.Add($analyzerFilePath.FullName) } } } } ``` #### uninstall.ps1 ``` param($installPath, $toolsPath, $package, $project) $analyzersPaths = Join-Path (Join-Path (Split-Path -Path $toolsPath -Parent) "analyzers" ) * -Resolve foreach($analyzersPath in $analyzersPaths) { # Uninstall the language agnostic analyzers. if (Test-Path $analyzersPath) { foreach ($analyzerFilePath in Get-ChildItem $analyzersPath -Filter *.dll) { if($project.Object.AnalyzerReferences) { $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) } } } } # $project.Type gives the language name like (C# or VB.NET) $languageFolder = "" if($project.Type -eq "C#") { $languageFolder = "cs" } if($project.Type -eq "VB.NET") { $languageFolder = "vb" } if($languageFolder -eq "") { return } foreach($analyzersPath in $analyzersPaths) { # Uninstall language specific analyzers. $languageAnalyzersPath = join-path $analyzersPath $languageFolder if (Test-Path $languageAnalyzersPath) { foreach ($analyzerFilePath in Get-ChildItem $languageAnalyzersPath -Filter *.dll) { if($project.Object.AnalyzerReferences) { try { $project.Object.AnalyzerReferences.Remove($analyzerFilePath.FullName) } catch { } } } } } ``` ### 7 結束語 本文講的SourceGenerator和語法分析器,如果你感興趣但在實驗中遇到困難,你可以下載WebApiClient的原始碼來直接體驗和除錯,然後依葫蘆畫瓢造自己的SourceGener