Incremental SourceGenerator?
Incremental SourceGeneratorはざっくり言うとパフォーマンスを改善したSourceGeneratorです!
Unity 2022.3.12f1以降 で利用可能です。
今回はIncremental SourceGeneratorを開発するためのプロジェクト構築と、
一部のエクステンションの導入、及び、導入したExtensionを使って簡単なGeneratorを作る方法を解説したいと思います!
Riderを利用した場合で解説していきます。
Solutionの準備
早速、Incremental SourceGeneratorを作成していきましょう!
Solution作成
なにはともあれ、まずはSolutionを作成します!
SDK:6.0
言語:C#
Framework:netstandard2.0
で作ります。
NuGetパッケージの追加
ActionsからNugetを開き、Microsoft.CodeAnalysis.CSharpの4.1.0を導入します。
同様にPolySharpの最新版もインストールしておきましょう。
csprojファイルの変更
Explorerの表示をFileSystem表示に切り替えて、Solution名.csproj ファイルを開きましょう。
以下を参考に、不足部分を追記してください!
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<!--ココカラ-->
<LangVersion>12</LangVersion>
<Nullable>enable</Nullable>
<IncludeBuildOutput>false</IncludeBuildOutput>
<DevelopmentDependency>true</DevelopmentDependency>
<IsRoslynComponent>true</IsRoslynComponent>
<!--ココマデ追記-->
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" Version="4.1.0" />
<PackageReference Include="PolySharp" Version="1.14.1">
<PrivateAssets>all</PrivateAssets>
<IncludeAssets>runtime; build; native; contentfiles; analyzers; buildtransitive</IncludeAssets>
</PackageReference>
</ItemGroup>
</Project>
Extensionの追加
Unityの都合で、Microsoft.CodeAnalysis.CSharpパッケージはver 4.1.0を導入しなければなりません。
ですが、4.3.0から導入されているForAttributeWithMetadataName
というメソッドが大変便利で、ぜひ使いたいです…!
そこで、以下のURLからファイルをDLしてプロジェクトに追加し、無理やり使えるようにします。
大元は、以下のRoslyn公式Gitから必要なcsファイルをDLして、一部をコメントアウトしたものになります。
基本的なクラスの構成
ここまでで、環境構築は完了しました!
もりもりGeneratorコードを書いていきましょう…!!
とは言え、見知らぬメソッドが多いので、
基本的なメソッドと、Providerの利用方法について簡単に解説します。
別途、デバッガーの導入方法も記事にしています。
効率が大きく上がるので、本格的な実装の前にデバッガーの環境構築をオススメします。
すべての処理は、Initializeメソッド内で行います。
ザックリ言うと、
- 現在のコードの情報を提供する
Provider
を生成し、 - Providerの情報を解釈してソースを作るActionを定義し、
- そのActionを、コードコンパイル時のパイプラインに登録する
という流れになります。
Generatorの最小構成はこんな感じになります。
[Generator(LanguageNames.CSharp)]
public class SourceGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
context.RegisterPostInitializationOutput(
static (context)=> {/* アトリビュートを作成する*/});
//Providerの生成
var provider = context.SyntaxProvider.ForAttributeWithMetadataName
(
context,
"作成したアトリビュートの名前を文字列で与える",
static (node, cancellation) => node is ClassDeclarationSyntax /*対象にしたい区分を指定する*/,
static (cont, cancellation) => cont /*データの変形が必要な場合は指定する*/
)
.Collect();
// 引数で与えたアクションをコンパイル時のパイプラインに登録
context.RegisterSourceOutput(
provider,
// 以下のStaticアクションが生成コードの定義
static (sourceProductionContext, metaDataArray) =>
{
/*収集したデータを使ってクラス等を生成する*/
});
}
}
順に詳細を追っていきます。
Action系の引数をstaticにしているのは、その方がパフォーマンス面で有利だからです。
Providerについて
Incremental Source Generatorでは、
Providerを生成してパイプラインに渡すことで、コードの解析情報にアクセスします。
contextからは、以下のProviderにアクセスできます。
- SyntaxProvider
- CompilationProvider
- AdditionalTextsProvider
- AnalyzerConfigOptionsProvider
- MetadataReferencesProvider
- ParseOptionsProvider
その中で、今回使っているのは、SyntaxProvider
です。
おそらくこれが最も使うことになるProviderかと思います。
今回はその中でも、先程導入したForAttributeWithMetadataName
メソッドの使い方について紹介していきます。
各Providerの詳細が知りたい型はこちらの資料をご覧ください
ForAttributeWithMetadataNameメソッド
IncrementalGeneratorInitializationContext
のメソッドであるForAttributeWithMetadataName
メソッドについて解説します。
このメソッドは、指定したアトリビュートのついた情報を取り出し、Providerとして返却します。
下記の通り、context.SyntaxProvider.ForAttributeWithMetadataName
で呼び出します。
//Providerの生成
var provider = context.SyntaxProvider.ForAttributeWithMetadataName
(
context,
"作成したアトリビュートの名前を文字列で与える",
static (node, cancellation) => node is ClassDeclarationSyntax /*対象にしたい区分を指定する*/,
static (cont, cancellation) => cont /*データの変形が必要な場合は指定する*/
)
最初の引数 : context
最初の引数にはcontextを与えます。
Initializeメソッドの引数として渡っているので、そのまま渡してください。
二番目の引数 : fullyQualifiedMetadataName
二番目の引数には検索したいアトリビュートの名前を文字列で渡します。名前空間も渡す必要があるのでご注意ください。
例えば以下のようなクラスを作ったとすると…
namespace SourceGeneratorSample
{
[AttributeUsage(AttributeTargets.Class,
Inherited = false, AllowMultiple = false)]
sealed class SampleAttribute : Attribute{}
}
与える文字列は"SourceGeneratorSample.SampleAttribute"
になります。
三番目の引数 : predicate
いわゆるpredicateです。Funcを使って要素を絞ることができます。
ここで、trueを返す要素のみが、providerに反映します。
SyntaxNodeとCancellationTokenを引数に取ります。
そのため、判定はSyntaxNodeから構文上可能な範囲に制限されます。
上記の (node, cancellation) => node is ClassDeclarationSyntax
のように、
SyntaxNodeの型判定をすることが多いようです。
AttributeTargets.Fieldで指定した要素をソートする場合、
VariableDeclaratorSyntaxで返ってくるのでご注意ください。
四番目の引数 : select
LINQのSelectのように、データの内容を編集してProviderにしたい場合に利用します。
.Collect()メソッド
前述のコードの末尾にくっついている
.Collect()
は、Provider内の要素を分割するためのメソッドです。
ソートされた複数の要素は、そのままだと、単一の大きな塊としてProviederに登録されてしまいます。
要素ごとに分割しておくことで、後々扱いやすくなるので、付けておきましょう。
.Collect()
を使うことで、各要素を別々のGeneratorAtributeSyntaxContext
に変換し、それを ImmutableArray
に格納することができます。
▼イメージ図
IncrementalValuesProvider<TSource> IncrementalValueProvider<ImmutableArray<TSource>>
┌───────────┐ ┌─────────────────────────┐
│ │ Collect<TSource> │ │
│ TSource ├─────────────────────────────────►│ ImmutableArray<TSource> │
│ │ │ │
└───────────┘ └─────────────────────────┘
3 Items Single Item
Item1 [Item1, Item2, Item3]
Item2
Item3
図は incremental-generators.mdより引用
The MIT License (MIT) Copyright (c) .NET Foundation and Contributors
Registerメソッド
続いて、コード生成方法を登録するメソッドについて、解説していきます。
context.RegisterPostInitializationOutpuメソッド
コード例の以下の部分です。
context.RegisterPostInitializationOutput(
static (context)=> {/* アトリビュートを作成する*/});
その名の通り、Initializationの後に実行されるコード生成の登録です。
RegisterPostInitializationOutputには、Providerは渡せません。
初回のみの起動になるので、コード生成のキーとなるアトリビュートの生成などにご利用ください。
context.RegisterSourceOutputメソッド
コード例の以下の部分です。コード生成を定義する本体部分になります。
// 引数で与えたアクションをコンパイル時のパイプラインに登録
context.RegisterSourceOutput(
provider,
// 以下のStaticアクションが生成コードの定義
static (sourceProductionContext, metaDataArray) =>
{
/*収集したデータを使ってクラス等を生成する*/
});
RegisterSourceOutputメソッドは、
Provderから解析したコンパイル情報を受け取り、
それを利用して、新しいコードを生成するための登録メソッドです。
最初の引数:source
この引数には、上記で生成したProviderを与えてください。
二番目の引数:action
このActionが、コードの生成方法を定義するActionになります。
Actionは二つの引数を持ちます。
-
Actionの1つ目の引数は SourceProductionContext です
その名の通り、実際にソースコードを生成するときに必要となるcontextです。Source Generators API
によって自動的に提供されるので、1つ目の引数には脳死でsourceProductionContext
と書いておきましょう。最終的には、このコンテキストに生成するコードのファイル名と、コード内容の文字列を渡してコード生成を行います。
-
Actionの2つ目の引数は、上記の
Provider<T>
が、T
として持っていた型が入ります。
.Collect()を使ってArrayに変換しておけば、ここでforeaceして各要素にアクセスすることができます。
メモリアロケーションの関係で、staticなactionにすることをオススメします。
GeneratorAttributeSyntaxContext構造体
今回紹介した方法を使うと、アクションの二つ目の引数Tは、GeneratorAttributeSyntaxContext
構造体となります。
この構造体は以下の4つのプロパティを持っています。
-
SyntaxNode
型 -
ISymbol
型 -
SemanticModel
型 -
ImmutableArray<AttributeData>
型
この情報を解析してコードを生成することになるので、軽く説明します!
実際に作るときには、これらの型からどのように欲しい情報が抽出できるか、ChatGPTに聞いてみるのがオススメです。
SyntaxNode
型
要素の構文的な側面を主に表します。
VariableDeclaratorSyntax
やClassDeclarationSyntax
など、各要素を表す継承クラスにキャストすると使いやすいです。
TargetNode
プロパティからアクセスできます。
ISymbol
型
各要素の識別情報や宣言の種類などを表現しています。
広範な情報を取り出すことができ、非常に有用です。
INamedTypeSymbol
や、IFieldSymbol
型にキャストすると使いやすいです。
TargetSymbol
プロパティからアクセスできます。
SemanticModel
型
コード全体の意味論的な構造を表現します。
現在の要素から横断して、関連する別要素などを追加取得したい場合などに有用です。
SemanticModel
プロパティからアクセスできます。
ImmutableArray<AttributeData>
型
このアトリビュート自体が付与された時のデータを保持しています。
例えば以下の様にすると、アトリビュートとともに渡された引数の情報を判定することができます。
Attributes
プロパティからアクセスできます。
foreach (var attributeData in AttributeDatas)
{
foreach (var arg in attributeData.ConstructorArguments)
{
//ここで、argに対して必要な判定が可能です。
}
}
コードの生成
Providerの情報を使って、生成したいコードの文字列を作ったら、
上記のsourceProductionContext
をつかてそれをコンパイル結果に組み込みます。
以下のようにAddSourceメソッドを使ってコードを生成します。
sourceProductionContext.AddSource
($"/*作りたいコード名*/.g.cs", SourceText.From(/*コード本体の文字列*/, Encoding.UTF8));
最小の構成
以上の技術を使って、クラス名だけを引用してpartialClassを生成するコードを書いてみました。
using System;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.DotnetRuntime.Extensions;
using Microsoft.CodeAnalysis.Text;
namespace IncrementalSourceGeneratorSample
{
[Generator(LanguageNames.CSharp)]
public class SourceGenerator : IIncrementalGenerator
{
public void Initialize(IncrementalGeneratorInitializationContext context)
{
//対象となるクラスを判別するアトリビュートを生成する
context.RegisterPostInitializationOutput( static x => SetAttribute(x));
//Providerを生成
var provider = context.SyntaxProvider.ForAttributeWithMetadataName
(
context,
"SourceGeneratorSample.SampleAttribute",
static (node, cancellation) => node is ClassDeclarationSyntax,
static (cont, cancellation) => cont
)
.Collect();
context.RegisterSourceOutput(
provider,
static (sourceProductionContext, metaDataArray) =>
{
//collectしたProviderをForeachループで一つずつ取り出す
foreach (var meta in metaDataArray)
{
//個別のproviderからシンボル情報を取り出す
var symbol = meta.TargetSymbol;
var name = symbol.Name;
//シンボルの情報を使って、コード本体の文字列を生成する
var source = $@"
public partial class {name}
{{
public void Hello()
{{
System.Console.WriteLine(""Hello, {name}!"");
}}
}}";
//生成した文字列をコードとして生成する
sourceProductionContext.AddSource($"{name}.g.cs", SourceText.From(source, Encoding.UTF8));
}
});
}
static void SetAttribute(IncrementalGeneratorPostInitializationContext context)
{
//Sampleというアトリビュートクラスの文字列を定義
const string AttributeText = @"
using System;
namespace SourceGeneratorSample
{
[AttributeUsage(AttributeTargets.Class,
Inherited = false, AllowMultiple = false)]
sealed class SampleAttribute : Attribute
{
public SampleAttribute()
{
}
}
}
";
//定義した文字列から、コードを作ってコンパイルする
context.AddSource
(
"SampleAttribute.cs",
SourceText.From(AttributeText,Encoding.UTF8)
);
}
}
}
このコードを、次のClass1を持つプロジェクトで走らせてみます。
[Sample]
public partial class Class1 { }
無事、以下のようなクラスが生成されました!!
public partial class Class1
{
public void Hello()
{
System.Console.WriteLine("Hello, Class1!");
}
}
最小構成でわかりやすく表示するために、
namespaceの対応や、クラスのアクセスレベルの対応などを行っていません。
以下のレポジトリではそれらの対応をしたコードを書いていますのでご参考ください。
ビルド時にCSC : error CS1617: Invalid option '12' for /langversion. Use '/langversion:?' to list supported values.
などのエラーが出る場合は、最新の.NetSDKをインストールして、MSBuildのバージョンを更新してください。
利用方法
お疲れ様でした!
ビルドして、DLLをUnity上に配置し、RoslynAnalyzorとして登録すれば、利用可能です!
Unity上での設定方法などは、以下URLにて詳しく解説してますので、必要に応じてご覧ください!
レポジトリ
最後に、同ジェネレーターを使用して作ったレポジトリを掲載しておきます!
いずれも比較的単純な実装例となるため、参考にし易いかと思います!
最も単純な例
▼ここまでの内容を反映したレポジトリ:最も単純な構成です
上記コードから、
namespaceへの対応と、クラスのアクセス修飾子の反映を施したコードに書き換えてあります。
フィールドの情報を元に生成している例
▼Propertyの自動実装を行うIncremental Source Generatorのレポジトリ
▼GetComponent系のボイラーコードを省略するIncremental Source Generator
ClassやInterfaceを元に生成している例 複数のProviderを結合している例
▼クラスやインターフェースの情報からNullObjectを自動生成するIncremental Source Generator
謝辞およびその他のリソース
参考にさせていただいたレポジトリ
本記事の執筆、及び上記レポジトリの作成にあたっては、hadashiA様のhadashiA様のVyaml内に使われているIncremental Source Generatorの実装を参考にさせていただきました。
この場を借りて深くお礼申し上げます。
SyntaxTreeの確認
SyntaxTreeの確認にはSharpLabが使えます。
SyntaxNode型をキャストして使うときに、どんな情報が含まれているか確認しておくと大変便利です!
また、RiderのPluginでSyntax Treeを表示してくれるPluginがありました。
もともとのPlugin開発が止まってしまっているので、Forkされている分を掲載します。
Forkの内部まで確認したわけではありませんので、一応ご利用は自己責任にてお願いいたします。
上記ポストに対するKoji Hasegawa様のリプライで知りました。感謝です!
私の手元のRider2023.2.1では無事動作しました