SourceGenerator入門指北

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。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章