SourceGenerator入門指北
阿新 • • 發佈:2021-01-25
### 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