1 SourceGenerator介紹
SourceGenerator於2020年4月29日在微軟的.net blog首次介紹,大概說的是開發者編可以寫分析器,在專案程式碼編譯時,分析器分析專案既有的靜態程式碼,允許新增原始碼到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之前有個分支做SourceGenerator的實驗,但遲遲沒有合併到master來。現在它已經合併到master,並以一個Extensions.SourceGenerator擴充套件包的方式出現,讓WebApiClientCore多一種代理類生成的方式選擇。這個擴充套件包編寫時非常簡單,我已經不想看以前是怎麼用Cecil為程式集插入靜態IL的程式碼了。
4 如何編寫xxxSourceGenerator
建立一個netstandard2.0的程式集
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<LangVersion>8.0</LangVersion>
<Nullable>enable</Nullable>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.8.0" PrivateAssets="all" />
<PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.3.2" PrivateAssets="all" />
</ItemGroup>
</Project>
實現ISyntaxReceiver,接收編譯時語法樹的遍歷
class xxxSyntaxReceiver : ISyntaxReceiver
{
/// <summary>
/// xxx感興趣的介面列表
/// </summary>
private readonly List<InterfaceDeclarationSyntax> interfaceSyntaxList = new List<InterfaceDeclarationSyntax>();
/// <summary>
/// 訪問語法樹
/// </summary>
/// <param name="syntaxNode"></param>
void ISyntaxReceiver.OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (syntaxNode is InterfaceDeclarationSyntax syntax)
{
this.interfaceSyntaxList.Add(syntax);
}
}
}
實現ISourceGenerator,且使用[Generator]特性
[Generator]
public class xxxSourceGenerator : ISourceGenerator
{
/// <summary>
/// 初始化
/// </summary>
/// <param name="context"></param>
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new xxxSyntaxReceiver());
}
/// <summary>
/// 執行
/// </summary>
/// <param name="context"></param>
public void Execute(GeneratorExecutionContext context)
{
if (context.SyntaxReceiver is xxxSyntaxReceiver receiver)
{
// 從receiver獲取你感興趣的語法節點
// 然後拼接成string的程式碼
// 把程式碼新增到context
context.AddSource("程式碼1的id","這裡是c#程式碼,會參與編譯的");
}
}
}
5 如何除錯xxxSourceGenerator
在被除錯專案以分析器方式引入xxxSourceGenerator專案
<ItemGroup>
<ProjectReference Include="..\xxxSourceGenerator\xxxSourceGenerator.csproj" OutputItemType="Analyzer" ReferenceOutputAssembly="false" />
</ItemGroup>
在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的原始碼來直接體驗和除錯,然後依葫蘆畫瓢造自己的SourceGenerator。