基于 Source Generators 做个 AOP 静态编织小实验

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(),"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,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> 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 生成结果不能查看

对使用生成器的人会比较麻烦,他不知道具体成什么样子了,特别是生成有错误的时候。

相关文章

项目中经常遇到CSV文件的读写需求,其中的难点主要是CSV文件...
简介 本文的初衷是希望帮助那些有其它平台视觉算法开发经验的...
这篇文章主要简单记录一下C#项目的dll文件管理方法,以便后期...
在C#中的使用JSON序列化及反序列化时,推荐使用Json.NET——...
事件总线是对发布-订阅模式的一种实现,是一种集中式事件处理...
通用翻译API的HTTPS 地址为https://fanyi-api.baidu.com/api...