ソースジェネレータ(Source Generator)は、コンパイラが生成したメタデータを使ってユーザコードを調査し、新たなコードを追加的に生成して、プログラムの他の部分と一緒にコンパイルするという動作を可能にする、C#コンパイラの新機能である。
F#の型プロバイダに着想を得たC#のソースジェネレータは、メタプログラミングを可能にするという同じ目標に応えたものでありながら、その方法はまったく異なっている。実際に、F#の型プロバイダが型やプロパティ、メソッドをインメモリで出力するのに対して、ソースプロバイダはC#のコードを生成してコンパイルプロセスに戻すのだ。
ソースジェネレータでは既存のコードを変更することはできない。新たなコードをコンパイルに追加するのみである。別の制限として、他のソースジェネレータが生成したコードに適用することもできない。これにより、それぞれのコードジェネレータは、その適用される順番に関係なく、同じコンパイル入力を扱うことが保証される。興味深い点として、ソースジェネレータはソースコードと関連するメタデータを調べるだけでなく、他のファイルにアクセスすることも可能である。
そもそもソースジェネレータは、技術的にはある程度可能ではあるとは言え、オプティマイザやコードインジェクタのようなコード書き換えツールとしての利用や、あるいは新たな言語機能の作成を目的として設計されたものではない。C#チームがソースジェネレータで明示的にターゲットにしたユースケースは、インターフェースの自動実装、データのシリアライゼーションといったものだ。もっと多くのユースケースと、それらを解決するための推奨アプローチに関する議論が、"source generator cookbook"に紹介されている。
ソースジェネレータはRoslynコードアナライザと強く関係しており、定義されたインターフェースからもそれは伺える。
namespace Microsoft.CodeAnalysis
{
public interface ISourceGenerator
{
void Initialize(InitializationContext context);
void Execute(SourceGeneratorContext context);
}
}
Initialize
メソッドはコンパイラによって呼び出される。この呼び出しを利用して、後で呼び出される一連のコールバックを登録する。コード生成を実際に行うのはExecute
メソッドである。このメソッドが受け取るSourceGeneratorContext
プロジェクトを使えば、現在のCompilation
オブジェクトへのアクセスが可能になる。
namespace Microsoft.CodeAnalysis
{
public readonly struct SourceGeneratorContext
{
public ImmutableArray<AdditionalText> AdditionalFiles { get; }
public CancellationToken CancellationToken { get; }
public Compilation Compilation { get; }
public ISyntaxReceiver?SyntaxReceiver { get; }
public void ReportDiagnostic(Diagnostic diagnostic) { throw new NotImplementedException(); }
public void AddSource(string fileNameHint, SourceText sourceText) { throw new NotImplementedException(); }
}
}
AddSource
を使えば、SourceGeneratorContext
オブジェクトを修正してコードを追加することができる。 すでに述べたように、ソースジェネレータの対象はC#ファイルのみではない。コンパイラに渡される任意のファイルを含むAdditionalFiles
コレクションにも、これが反映されている。
ここまでをまとめると、"hello world"プログラム用の簡単なソースジェネレータを次のように定義することができる。
using System;
using System.Collections.Generic;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.Text;
namespace SourceGeneratorSamples
{
[Generator]
public class HelloWorldGenerator : ISourceGenerator
{
public void Execute(SourceGeneratorContext context)
{
// begin creating the source we'll inject into the users compilation
var sourceBuilder = new StringBuilder(@"
using System;
namespace HelloWorldGenerated
{
public static class HelloWorld
{
public static void SayHello()
{
Console.WriteLine(""Hello from generated code!"");
Console.WriteLine(""The following syntax trees existed in the compilation that created this program:"");
");
// using the context, get a list of syntax trees in the users compilation
var syntaxTrees = context.Compilation.SyntaxTrees;
// add the filepath of each tree to the class we're building
foreach (SyntaxTree tree in syntaxTrees)
{
sourceBuilder.AppendLine($@"Console.WriteLine(@"" — {tree.FilePath}"");");
}
// finish creating the source to inject
sourceBuilder.Append(@"
}
}
}");
// inject the created source into the users compilation
context.AddSource("helloWorldGenerator", SourceText.From(sourceBuilder.ToString(), Encoding.UTF8));
}
public void Initialize(InitializationContext context)
{
// No initialization required for this one
}
}
}
Microsoftは多数の導入サンプルをリリースして、この新機能の使用方法を開発者に紹介している。
ソースジェネレータは.NET 5プレビュー版と、最新のVisual Studioプレビュー版で使用することができる。機能的にはまだ初期段階であるため、C# 9のGAに向けた機能リリースの中で、APIや特徴が変更される可能性がある。