6
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

【Unity用】Incremantal Source Generator プロジェクトの作り方

Last updated at Posted at 2024-01-22

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
で作ります。

image.png

NuGetパッケージの追加

ActionsからNugetを開き、Microsoft.CodeAnalysis.CSharpの4.1.0を導入します。

名称未設定-1.png

同様にPolySharpの最新版もインストールしておきましょう。
image.png

csprojファイルの変更

Explorerの表示をFileSystem表示に切り替えて、Solution名.csproj ファイルを開きましょう。
以下を参考に、不足部分を追記してください!

image.png

<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して、一部をコメントアウトしたものになります。

大元のURL郡

https://github.com/dotnet/runtime/blob/1473deaa50785b956edd7d078e68c0581c1b4d95/src/libraries/Common/src/Roslyn/GetBestTypeByMetadataName.cs

https://github.com/dotnet/runtime/blob/1473deaa50785b956edd7d078e68c0581c1b4d95/src/libraries/Common/src/Roslyn/CSharpSyntaxHelper.cs

https://github.com/dotnet/runtime/blob/1473deaa50785b956edd7d078e68c0581c1b4d95/src/libraries/Common/src/Roslyn/GlobalAliases.cs

https://github.com/dotnet/runtime/blob/main/src/libraries/Common/src/Roslyn/Hash.cs

https://github.com/dotnet/runtime/blob/1473deaa50785b956edd7d078e68c0581c1b4d95/src/libraries/Common/src/Roslyn/SyntaxValueProvider.ImmutableArrayValueComparer.cs

https://github.com/dotnet/runtime/blob/1473deaa50785b956edd7d078e68c0581c1b4d95/src/libraries/Common/src/Roslyn/ISyntaxHelper.cs

https://github.com/dotnet/runtime/blob/1473deaa50785b956edd7d078e68c0581c1b4d95/src/libraries/Common/src/Roslyn/SyntaxValueProvider_ForAttributeWithMetadataName.cs

https://github.com/dotnet/runtime/blob/1473deaa50785b956edd7d078e68c0581c1b4d95/src/libraries/Common/src/Roslyn/SyntaxValueProvider_ForAttributeWithSimpleName.cs

https://github.com/dotnet/runtime/blob/1473deaa50785b956edd7d078e68c0581c1b4d95/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/ValueListBuilder.cs

https://github.com/dotnet/roslyn/blob/main/src/Workspaces/SharedUtilitiesAndExtensions/Compiler/Core/Utilities/SymbolVisibility.cs

https://github.com/dotnet/runtime/blob/main/src/libraries/System.Private.CoreLib/src/System/Collections/Generic/ValueListBuilder.Pop.cs#L11

基本的なクラスの構成

ここまでで、環境構築は完了しました!
もりもりGeneratorコードを書いていきましょう…!!
とは言え、見知らぬメソッドが多いので、
基本的なメソッドと、Providerの利用方法について簡単に解説します。

別途、デバッガーの導入方法も記事にしています
効率が大きく上がるので、本格的な実装の前にデバッガーの環境構築をオススメします。

すべての処理は、Initializeメソッド内で行います。
ザックリ言うと、

  1. 現在のコードの情報を提供するProviderを生成し、
  2. Providerの情報を解釈してソースを作るActionを定義し、
  3. その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

要素の構文的な側面を主に表します。
VariableDeclaratorSyntaxClassDeclarationSyntaxなど、各要素を表す継承クラスにキャストすると使いやすいです。
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のバージョンを更新してください。
image.png

利用方法

お疲れ様でした!
ビルドして、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では無事動作しました

6
8
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
8

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?