基於 Source Generators 做個 AOP 靜態編織小實驗

victor.x.qu發表於2020-12-16

0. 前言

上接:用 Roslyn 做個 JIT 的 AOP

作為第二篇,我們基於Source Generators做個AOP靜態編織小實驗。

內容安排如下:

  • source generators 是什麼?
  • 做個達到上篇Jit 一樣的效果的demo
  • source generators還存在什麼問題?

1. Source Generators 是什麼?

1.1 核心目的

開啟dotnet平臺的編譯時超程式設計功能,

讓我們能在編譯時期動態建立程式碼,

同時考慮IDE的整合,讓體驗更舒適。

官方文件:https://github.com/dotnet/roslyn/blob/master/docs/features/source-generators.md

展開我們思想的翅膀

我們能以此做各種事情:

  • 生成實體json 等序列化器程式碼
  • AOP
  • 介面定義生成httpclient呼叫程式碼
  • 等等

如下是官方認為會受益的部分功能列表:

  • ASP.Net: Improve startup time
  • Blazor and Razor: Massively reduce tooling burden
  • Azure Functions: regex compilation during startup
  • Azure SDK
  • gRPC
  • Resx file generation
  • System.CommandLine
  • Serializers
  • SWIG

1.2 目前其設計和使用準則

允許開發者能在編譯時動態建立新增新程式碼到我們程式裡面

只能新增程式碼,不能修改已有程式碼

當無法生成源時,生成器應當產生診斷資訊,通知使用者問題所在。

可能訪問其他檔案非c#原始碼檔案。

無序執行模式,每個生成器都只能擁有相同的輸入編譯,即不能用其他生成器的生成結果進行再次生成。

生成器的執行類似於分析器。

2. 實驗:代理模式的靜態編織

2.1 建立一個Source Generators專案

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <TargetFramework>netstandard2.0</TargetFramework>
    <LangVersion>8.0</LangVersion>
  </PropertyGroup>
  <PropertyGroup>
    <RestoreAdditionalProjectSources>https://pkgs.dev.azure.com/dnceng/public/_packaging/dotnet5/nuget/v3/index.json ;$(RestoreAdditionalProjectSources)</RestoreAdditionalProjectSources>
  </PropertyGroup>
  <ItemGroup>
    <PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="3.6.0" PrivateAssets="all"/>
    <PackageReference Include="Microsoft.CodeAnalysis.Analyzers" Version="3.0.0" PrivateAssets="all" />
  </ItemGroup>
  
</Project>

2.2 建立SourceGenerator

需要繼承 Microsoft.CodeAnalysis.ISourceGenerator

namespace Microsoft.CodeAnalysis
{
    public interface ISourceGenerator
    {
        void Initialize(InitializationContext context);
        void Execute(SourceGeneratorContext context);
    }
}

並通過[Generator]標識啟用

所以我們就可以做一個這樣的代理生成器:

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
namespace AopAnalyzer
{
    [Generator]
    public class ProxyGenerator : ISourceGenerator
    {
        public void Execute(SourceGeneratorContext context)
        {
            // retreive the populated receiver
            if (!(context.SyntaxReceiver is SyntaxReceiver receiver))
                return;
            try
            {
                // 簡單測試aop 生成
                Action<StringBuilder, IMethodSymbol> beforeCall = (sb, method) => { };
                Action<StringBuilder, IMethodSymbol> afterCall = (sb, method) => { sb.Append("r++;"); };
                // 獲取生成結果
                var code = receiver.SyntaxNodes
                 .Select(i => context.Compilation.GetSemanticModel(i.SyntaxTree).GetDeclaredSymbol(i) as INamedTypeSymbol)
                 .Where(i => i != null && !i.IsStatic)
                 .Select(i => ProxyCodeGenerator.GenerateProxyCode(i, beforeCall, afterCall))
                 .First();
                context.AddSource("code.cs", SourceText.From(code, Encoding.UTF8));
            }
            catch (Exception ex)
            {
                // 失敗彙報
                context.ReportDiagnostic(Diagnostic.Create(new DiagnosticDescriptor("n001", ex.ToString(), ex.ToString(), "AOP.Generate", DiagnosticSeverity.Warning, true), Location.Create("code.cs", TextSpan.FromBounds(0, 0), new LinePositionSpan())));
            }
        }
        public void Initialize(InitializationContext context)
        {
            // Register a syntax receiver that will be created for each generation pass
            context.RegisterForSyntaxNotifications(() => new SyntaxReceiver());
        }
        /// <summary>
        /// 語法樹定義收集器,可以在這裡過濾生成器所需
        /// </summary>
        internal class SyntaxReceiver : ISyntaxReceiver
        {
            internal List<SyntaxNode> SyntaxNodes { get; } = new List<SyntaxNode>();
            public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
            {
                if (syntaxNode is TypeDeclarationSyntax)
                {
                    SyntaxNodes.Add(syntaxNode);
                }
            }
        }
    }
}

具體的代理程式碼生成邏輯:

using Microsoft.CodeAnalysis;
using System;
using System.Linq;
using System.Text;
namespace AopAnalyzer
{
    public static class ProxyCodeGenerator
    {
        public static string GenerateProxyCode(INamedTypeSymbol type, Action<StringBuilder, IMethodSymbol> beforeCall, Action<StringBuilder, IMethodSymbol> afterCall)
        {
            var sb = new StringBuilder();
            sb.Append($"namespace {type.ContainingNamespace.ToDisplayString()} {{");
            sb.Append($"{type.DeclaredAccessibility.ToString().ToLower()} class {type.Name}Proxy : {type.ToDisplayString()} {{ ");
            foreach (var method in type.GetMembers().Select(i => i as IMethodSymbol).Where(i => i != null && i.MethodKind != MethodKind.Constructor))
            {
                GenerateProxyMethod(beforeCall, afterCall, sb, method);
            }
            sb.Append(" } }");
            return sb.ToString();
        }
        private static void GenerateProxyMethod(Action<StringBuilder, IMethodSymbol> beforeCall, Action<StringBuilder, IMethodSymbol> afterCall, StringBuilder sb, IMethodSymbol method)
        {
            var ps = method.Parameters.Select(p => $"{p.Type.ToDisplayString()} {p.Name}");
            sb.Append($"{method.DeclaredAccessibility.ToString().ToLower()} override {method.ReturnType.ToDisplayString()} {method.Name}({string.Join(",", ps)}) {{");
            sb.Append($"{method.ReturnType.ToDisplayString()} r = default;");
            beforeCall(sb, method);
            sb.Append($"r = base.{method.Name}({string.Join(",", method.Parameters.Select(p => p.Name))});");
            afterCall(sb, method);
            sb.Append("return r; }");
        }
    }
}

可以看到和之前jit的程式碼非常相似

2.3 測試一下

2.3.1 新建測試專案

<Project Sdk="Microsoft.NET.Sdk">
  <PropertyGroup>
    <OutputType>Exe</OutputType>
    <TargetFramework>netcoreapp3.1</TargetFramework>
    <LangVersion>preview</LangVersion>  //新版本才有哦,現在還未正式釋出
  </PropertyGroup>
  <ItemGroup>
    <ProjectReference Include="..\AopAnalyzer\AopAnalyzer.csproj" 
                      OutputItemType="Analyzer"
                      ReferenceOutputAssembly="false" />  //設定為分析器專案
  </ItemGroup>
</Project>

2.3.2 測試程式碼

using System;
namespace StaticWeaving_SourceGenerators
{
    static class Program
    {
        static void Main(string[] args)
        {
            var proxy = new RealClassProxy(); // 對,生成的新程式碼可以ide裡面直接用,就是這麼強大,只要編譯一次就看的到了
            var i = 5;
            var j = 10;
            Console.WriteLine($"{i} + {j} = {(i + j)}, but proxy is {proxy.Add(i, j)}");
            Console.ReadKey();
        }
    }
  
    public class RealClass
    {
        public virtual int Add(int i, int j)
        {
            return i + j;
        }
    }
}

輸出結果:

5 + 10 = 15, but proxy is 16

cpu 和記憶體,自然完美:

完整的demo 放在 https://github.com/fs7744/AopDemoList

3. Source Generators 還有什麼嚴重的缺陷呢?

3.1 不能引入其他程式集,比如nuget包

比如我們引入Newtonsoft.Json,就會造成如下編譯異常:

System.IO.FileNotFoundException: 未能載入檔案或程式集“Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed”或它的某一個依賴項。系統找不到指定的檔案。

這就造成我們很難利用現有的包做各種事情,以及怎麼把我們的程式碼生成器提供給別人使用了

有同學就這一點提了issue : https://github.com/dotnet/roslyn/issues/45060

感興趣的同學可以去讚一讚

3.2 不能debug(其實我接受這點)

可以通過UT測試 debug的

3.3 生成結果不能檢視

對使用生成器的人會比較麻煩,他不知道具體成什麼樣子了,特別是生成有錯誤的時候。

相關文章