上接:用 Roslyn 做个 JIT 的 AOP
作为第二篇,我们基于Source Generators做个AOP静态编织小实验。
内容安排如下:
- source generators 是什么?
- 做个达到上篇Jit 一样的效果的demo
- 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 放在 github.com/fs7744/AopD…
3. Source Generators 还有什么严重的缺陷呢?3.1 不能引入其他程序集,比如nuget包
比如我们引入Newtonsoft.Json,就会造成如下编译异常:
System.IO.FileNotFoundException: 未能加载文件或程序集“Newtonsoft.Json, Version=12.0.0.0, Culture=neutral, PublicKeyToken=30ad4fe6b2a6aeed”或它的某一个依赖项。系统找不到指定的文件。
这就造成我们很难利用现有的包做各种事情,以及怎么把我们的代码生成器提供给别人使用了
有同学就这一点提了issue : github.com/dotnet/rosl…
感兴趣的同学可以去赞一赞
3.2 不能debug(其实我接受这点)
可以通过UT测试 debug的
3.3 生成结果不能查看
对使用生成器的人会比较麻烦,他不知道具体成什么样子了,特别是生成有错误的时候。