https://github.com/dotnet/roslyn/blob/main/docs/features/source-generators.md
源码生成器似乎还没有设计完,原文章可能还会更新(此翻译基于2021/05/11的版本)。

警告:Source Generators(V1)已知会导致vs性能变慢,应该使用Incremental Generators(V2)代替。

Summary

源代码生成器旨在启用编译时元编程,即可以在编译时创建并添加到编译中的代码。源代码生成器将能够在运行前读取编译内容,并访问任何其他文件,从而使生成器能够自省用户C#代码和特定于生成器的文件。

Note: This proposal is separate from the previous generator design

High Level Design Goals

  • 生成器生成一个或多个字符串,这些字符串表示要添加到编译中的C#源代码
  • 仅限明确的可加性。生成器可以将新源代码添加到编译中,但能修改现有用户代码。
  • 可以产生诊断结果。当无法生成源代码时,生成器可以将问题通知用户。
  • 可以访问其他文件,即非C#源文本。
  • 无序运行,每个生成器将看到相同的输入编译,无法访问其他源生成器创建的文件。
  • 用户通过程序集列表指定要运行的生成器,这与分析器非常相似。

Implementation

最简单的源代码生成器是实现 Microsoft.CodeAnalysis.ISourceGenerator

  1. namespace Microsoft.CodeAnalysis
  2. {
  3. public interface ISourceGenerator
  4. {
  5. void Initialize(GeneratorInitializationContext context);
  6. void Execute(GeneratorExecutionContext context);
  7. }
  8. }

生成器实现是在传递给编译器的外部程序集中定义的,使用用于诊断分析器的相同-analyzer:选项。实现需要使用Microsoft.CodeAnalysis.GeneratorAttribute属性进行批注。

程序集可以包含诊断分析器和源生成器的混合。由于生成器是从外部部件加载的,因此生成器不能在定义它的程序集中使用。

ISourceGenerator有一个由宿主(IDE或命令行编译器)仅调用一次的Initialize方法。Initialize传递GeneratorInitializationContext的一个实例,生成器可以使用该实例注册一组回调,这些回调会影响将来的生成过程。

主生成过程通过Execute方法进行。Execute传递GeneratorExecutionContext的一个实例,该实例提供对当前Compliaction的访问,并允许生成器通过添加源和报告诊断来更改结果输出编译。

生成器还能够访问通过AdditionalFiles集合传递给编译器的任何AnalyzerAdditionalFiles,从而允许生成决策,而不仅仅是基于用户的C#代码。

  1. namespace Microsoft.CodeAnalysis
  2. {
  3. public readonly struct GeneratorExecutionContext
  4. {
  5. public ImmutableArray<AdditionalText> AdditionalFiles { get; }
  6. public CancellationToken CancellationToken { get; }
  7. public Compilation Compilation { get; }
  8. public ISyntaxReceiver? SyntaxReceiver { get; }
  9. public void ReportDiagnostic(Diagnostic diagnostic) { throw new NotImplementedException(); }
  10. public void AddSource(string fileNameHint, SourceText sourceText) { throw new NotImplementedException(); }
  11. }
  12. }

假设某些生成器想要生成多个SourceText,例如,以1:1的映射生成其他文件。AddSourcefileNameHint参数旨在解决此问题:

  1. 如果生成的文件被发送到磁盘,那么能够放入一些有区别的文本可能会很有用。例如,如果您有两个.resx文件,生成仅名为ResxGeneratedFile1.csResxGeneratedFile2.cs的文件不是很有用—如果您有两个分别名为“Strings”和“Icons”的.resx文件,您可能希望它类似于ResxGeneratedFile-Strings.csResxGeneratedFile-Icon.cs
  2. IDE需要一些“稳定”标识符的概念。源代码生成器给IDE带来了一些有趣的问题:例如,用户希望能够在生成的文件中设置断点。如果一个源生成器输出多个文件,我们需要知道哪个是哪个,这样我们才能知道断点对应的是哪个文件。当然,如果源生成器的输入发生更改,它可以停止发出文件(如果删除.resx,则与其关联的生成文件也将消失),但这在这里给了我们一些控制权。

这被称为“提示(hint)”,因为编译器被隐式地允许以其最终需要的方式控制文件名,并且如果两个源生成器给出相同的“提示(hint)”,它仍然可以根据需要用任何种类的前缀/后缀来区分它们。

IDE Integration

支持生成器的一个更复杂的方面是在Visual Studio中实现高保真体验。为了确定代码的正确性,期望所有生成器都必须运行。显然,在每次击键时运行每个生成器,并且仍然在IDE中保持可接受的性能级别是不切实际的。

Progressive complexity opt-in

渐进式复杂性选择加入

取而代之的是,预计源代码生成器将采用“选择加入”的方法来启用IDE。

默认情况下,仅实现ISourceGenerator的生成器不会看到IDE集成,并且仅在构建时正确。根据与第一方客户的对话,有几种情况下这就足够了。

然而,对于诸如code first grpc这样的场景,特别是Razor和Blazor,集成开发环境需要能够在编辑这些文件类型时随手(on-the-fly)生成代码,并近乎实时地将更改反映到集成开发环境中的其他文件。

我们的建议是拥有一组可以选择实现的高级回调,这将允许IDE查询生成器以决定在任何特定编辑的情况下需要运行什么。

例如,将导致在保存第三方文件后运行生成的扩展可能如下所示:

  1. namespace Microsoft.CodeAnalysis
  2. {
  3. public struct GeneratorInitializationContext
  4. {
  5. public void RegisterForAdditionalFileChanges(EditCallback<AdditionalFileEdit> callback){ }
  6. }
  7. }

这将允许生成器在初始化期间注册一个回调,该回调将在每次其他文件更改时被调用。

预计会有不同级别的选择加入,可以将其添加到生成器中以实现所需的特定性能级别。

这些API到底会是什么样子仍然是一个悬而未决的问题,预计在知道它们的确切形状之前,我们需要制作一些真实的生成器的原型。

Output files

希望生成的源文本在生成后可供检查,或者作为创建生成器的一部分,或者查看由第三方生成器生成的代码。

默认情况下,生成的文本将保存到CommandLineArguments.OutputDirectory中的GeneratedFiles/{GeneratorAssemblyName}子文件夹中。GeneratorExecutionContext.AddSource中的fileNameHint将用于创建唯一名称,并在需要时应用适当的冲突重命名。例如,在Windows上,从C#项目的MyGenerator.dllAddSource("MyCode", ...);的调用可能被持久化为obj/debug/GeneratedFiles/MyGenerator.dll/MyCode.cs.

文件输出不是基于命令行或基于IDE的生成的正确功能所必需的,如果需要,可以完全禁用。IDE将处理生成的源文本的内存副本(用于“查找所有引用”、断点等)。并定期刷新对磁盘的任何更改。

为了支持这样的用例,即用户希望生成源文本,然后将生成的文件提交给源代码管理,我们将允许通过适当的命令行开关更改生成的文件的位置,并匹配MSBuild属性(命名仍有待确定)。

在这些情况下,用户是否希望在将来再次生成文件(在这种情况下,它们仍将被生成,但输出到受源代码控制的位置),或移除生成器并作为一个时间步骤执行操作,将由用户决定。

例如,在基于磁盘的生成文件中设置断点的操作将如何起作用,目前还是一个悬而未决的问题。

TK:我们如何保存PDB/源链接等?

Editing experiences for third party languages

第三方语言的编辑体验
源代码生成器将支持的一个有趣场景实质上是将C#“嵌入”到其他语言中(反之亦然)。这就是今天Razor的工作方式,Razor团队在Visual Studio中维护了大量的语言服务投资来实现这一点。

这个项目的一个可能的目标是找到一种通用的方式来表示这一点:这将允许Razor团队减少他们的工具投资,同时允许第三方有机会实现相同类型的体验(包括“转到定义”、“查找所有引用”等)。相对便宜。

目前的想法是给生成器提供某种形式的“侧通道(side-channel)”。当生成器发出源文本时,它将指示这是从原始文档中的什么位置生成的。这将允许编译器API将例如生成的Symbol跟踪为具有表示第三方源文本(例如.cshtml文件中的Razor标签)范围的OriginalDefinition

我们讨论了通过#pragma将其直接嵌入到源文本中,但这需要更改语言,并将该特性限制在特定版本的C#中。其他注意事项可以是特殊形成的注释或#if false--块。一般来说,在生成的文本中,“侧通道(side-channel)”方法似乎比特制的语法更可取。

这不一定是Source Generators成功所必需的目标;Razor的语言服务可以进行更新,以便在证明是不可行的情况下与源代码生成器一起工作,但这肯定是我们想要考虑的工作的一部分。

MSBuild Integration

预计生成器将需要某种形式的配置系统,我们打算允许某些属性从MSBuild流过以促进这一点。

Note: This is still under design and open to change.

Performance targets

最终,该功能的性能将在一定程度上取决于客户编写的生成器的性能。渐进式选择加入,默认情况下仅在构建时使用,这将使IDE能够缓解第三方生成器带来的许多潜在性能问题。但是,仍然存在第三方生成器会给IDE带来不可接受的性能问题的风险,该功能的设计需要牢记这一点。

对于第一方生成器,特别是Razor和Blazor,我们的目标是最低限度地与当前用户看到的现有性能相匹配。由于较少的通信开销和重复工作,预计即使是基于自然的生成器的实现也会比现有工具执行得更快,但是提高这些体验的速度并不是本项目的主要目标。

Language Changes

此设计目前并不建议更改语言,它纯粹是一种编译器功能。以前的源生成器设计引入了replaceoriginal关键字。此建议删除了这些,因为生成的源码纯粹是附加的,因此不需要它们。我们希望现有使用分部定义可以实现大多数场景;作为V1,我们希望在这种状态下发布。如果后来显示了使用V1方法无法实现的具体场景,我们会考虑允许将其修改为V2。

Use cases

我们已经确定了几个将受益于源生成器的第一方和第三方候选者:

  • 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

Discussion / Open Issues / TODOs:

Interface vs Class for ISourceGenerator:
我们讨论了这是一个接口还是类。分析器选择了一个抽象基类,但是我们不确定我们最终会需要什么,因为最终我们只有一个关于这个的方法。将其保留为接口也更为自然,因为我们还有其他接口也可以实现此接口,以实现可选的点亮。

IDependsOnCompilationGenerator:
我们确实讨论了是否应该有一个IDependsOnCompilationGenerator来正式声明您实际上使用了编译。毕竟,如果您不使用编译,那么我们知道您在IDE中的性能将大大简化。但是,我们读取额外文件的每个场景都需要编译,所以我们只是不确定这会带来什么。

Breakpoints in generated files:
我们是否将其映射回内存中的文件?

Should generators be push or pull:
源生成器基于Pull,分析器基于Push(基于注册)。我们是否也应该对生成器使用基于推送的模型?

  • 如果我们沿着基于推送的模型向下走,遍历树应该确保继续为尽可能多的节点生成事件,即使有错误,因为生成器通常会在存在的情况下工作
  • 我们今天用于分析器的事件可能需要更多的工作来生成,因为我们希望分析器在完全编译期间运行,而生成器可能甚至不想构造符号表
  • 渐进式性能选择加入模型在基于推送的模型中可能工作得更好,因为您只需要注册您关心的内容

Should we share more with the analyzer type hierarchy?:
我们是否应该与分析器类型层次结构共享更多内容?
我们仍然需要区分分析器和生成器,因为它们将在不同的时间生成(生成器诊断仅在第一次编译时生成,分析器诊断仅在第二次编译时生成)。
image.png

Can we predict how often some of our sample customers (Razor?) will have to run the generators?:
我们能预测我们的一些样本客户(Razor?)多久一次将不得不运行发电机?
他们现在无法预测这一点,而且将计时器合并到他们的当前生成器中使得预测仅基于事件的生成的结果变得非常困难。

Do we have a priority list of the most important customers?:
我们有最重要客户的优先列表吗?
没有,我们应该计算出优先级,以便确定功能的优先级。

Security Review:
安全审查
发电机是否会带来任何尚未通过分析器和Nuget构成的新安全风险?