【Unity】ソースジェネレーターをもうちょっと簡単に使う

Unity で Roslyn ソースジェネレーターを使おうと思うと、別プロジェクトで .dll 生成 → ソリューションファイルの編集云々、手間の方が掛かるんじゃ？？ という状況ですね。

<!--
> .dll は参照先のプロジェクトで握りっぱなしになるので更新したら再起動が必要とかなんとか。
-->

加えてソースジェネレーターに求めるものは `.cs` ファイルの更新をフックに別ファイルを生成して `partial` クラスで宣言を追加するだけ、って場合がほとんど。

それなら Unity の `AssetImporter` でいっか、と思い至った結果がコレ 👇

https://github.com/sator-imaging/Unity-AltSourceGenerator



<!--
- ✅ Unity パッケージマネージャー対応
- ✅ 面倒な IDE 環境セットアップが不要
- ✅ 追加ライブラリのインストールも不要


Roslyn が使いやすくなるまでの繋ぎ、ソースジェネレーター体験版としては良いんじゃないだろうか。Roslyn のソースジェネレーターは作るのとデバッグが面倒なのがねーー。誰かが作ってくれたのを使う分には良いんだけども。
-->



<p><details lang="ja" --open><summary>ソースジェネレーター雑感</summary>

メソッド名を直書きじゃなくリファクタリングに対応できるようにすると、どうしてもコードが長くなってしまう。元ファイルの `using` 丸ごと持ってくる手もあるけど、クラス内 Enum の宣言が自他ライブラリで結構あって難しい。

結局開発機で実行だから別にええやん、でトリッキーなユーティリティーメソッドを追加することに。本体を作りながらソースジェネレーターも弄る、だとリファクタリング対応はしておくと便利なんだ。

早く `fullnameof(...)` が欲しい。

```csharp
$"{nameof(MyNamespace)}.{nameof(MyNamespace.MyClass)}.{nameof(MyNamespace.MyClass.MyProperty)}"

// クラスのフルネームは何とかなるけど、IL言語的にはOK、ユーザーレベルでは使えない名前で出てくることもある。
$"{typeof(MyNamespace.MyClass).FullName}.{nameof(MyNamespace.MyClass.MyProperty)}"
```

既存のライブラリ・アセット・フレームワークに何か追加したい、と思っても出来ないのももどかしい。`[InternalsVisibleTo()]` 的なものが欲しいね。`[assembly: AllowPartialDefinitionTo("...")]` みたいな。

今後ソースジェネレーターが普及すれば、極々たまーーに見かけるクラス修飾子の調整用プリプロセッサシンボルを付けるのがマナーになるか？？

```csharp
namespace ToaruSDK
{
    public
#if TOARU_SDK_ALLOW_SOURCE_GENERATOR
    partial
#endif
    class Honyarara
    {
    }
}
```

<!------- End of Details JA Tag -------></details></p>



# メソッド生成

ターゲットのクラスに `Panic()` メソッドを追加するサンプル。`StringBuilder` が渡されるので書き込んで true を返せば `context.OutputPath` に内容を書き込み、false を返せば書き込みを中止。


```csharp
public class PanicMethodGenerator
{
    static string OutputFileName() => "PanicMethod.cs";  // 出力先 -> PanicMethod.<TargetClass>.<GeneratorClass>.g.cs

    static bool Emit(USGContext context, StringBuilder sb)
    {
        if (!context.TargetClass.IsClass || context.TargetClass.IsAbstract)
            return false;  // return false to tell USG doesn't write file.

        // コード生成   $@"..." が Unity 2021 で使えるの知らなかった。
        sb.Append($@"
namespace {context.TargetClass.Namespace}
{{
    internal partial class {context.TargetClass.Name}
    {{
        public void Panic() => throw new System.Exception();
    }}
}}
");
        return true;
    }
}
```



## 使い方

対象のクラスに `[UnitySourceGenerator(typeof(PanicMethodGenerator)]` アトリビュートを付ける。


```csharp
using SatorImaging.UnitySourceGenerator;

namespace Sample
{
    // ジェネレーターの型を指定。
    [UnitySourceGenerator(typeof(PanicMethodGenerator), OverwriteIfFileExists = false)]
    internal partial class MethodGeneratorSample
    {
    }

}
```



## 生成されたコード

見たまんま出てきます。`<auto-generated/>` タグは渡される `StringBuilder` に最初から入ってるので不要なら `sb.Clear()`。


```csharp
// <auto-generated>PanicMethodGenerator</auto-generated>

namespace Sample
{
    internal partial class MethodGeneratorSample
    {
        public void Panic() => throw new System.Exception();
    }
}
```



## 汎用ジェネレーター

ビルドイベント時に生成するようなものを作る時のサンプル。ランタイム上で使えない `UnityEditor.AssetDatabase` を使って GUID 一覧を作りたいとか、リソースのハッシュ値を生成したいとか、そういう用途。


```csharp
using System.Text;
using SatorImaging.UnitySourceGenerator;

// クラス指定が無ければ自分自身をジェネレーターとして使う。
[UnitySourceGenerator(OverwriteIfFileExists = false)]
class MinimalGenerator
{
    static string OutputFileName() => "Test.cs";  // 出力先 -> Test.<ClassName>.g.cs

    static bool Emit(USGContext context, StringBuilder sb)
    {
        // write content into passed StringBuilder.
        sb.AppendLine($"Asset Path: {context.AssetPath}");
        sb.AppendLine($"Hello World from {typeof(MinimalGenerator)}");

        // you can modify output path. initial file name is that USG updated.
        // NOTE: USG doesn't care the modified path is valid or not.
        context.OutputPath += "_MyFirstTest.txt";

        // return true to tell USG to write content into OutputPath. false to do nothing.
        return true;
    }
}
```



### 生成されたコード

```csharp
// <auto-generated>MinimalGenerator</auto-generated>

Asset Path: Assets/Scripts/MinimalGenerator.cs
Hello World from Sample.MinimalGenerator
```



# 糖衣構文系メソッド

`System.Reflection` 系のユーティリティーと `StringBuilder` の拡張メソッド群。

`usg` は特殊で、クラス名やら何やらのリファクタリングに強いジェネレーターにすると読みづらくなってしまうのを緩和するためのモノ。

`{typeof(MyClass).FullName}.{nameof(MyClass.Property)}` どんどん長くなるだけなら良いけどクラス内クラスとか構造体の名前が + 付きの C# 開発時には使えない、不正な状態で出てくる。その他にもジェネリッククラスへの対応（してないけど）とかなんとか、結局何かが必要になる。それならばと可能な限り短く書けるようにした。

インデント系はトリッキーだけど開発機での実行なのでまあ良し。

- `StringBuilder` Extentions
    - `IndentLine` / `IndentAppend`
    - `IndentLevel` / `IndentChar` / `IndentSize`
    - `IndentBegin` / `IndentEnd`
- `USGFullNameOf`
    - `usg`
- `USGReflection`
    - `GetAllPublicInstanceFieldAndProperty`
    - `TryGetFieldOrPropertyType`
    - `GetEnumNamesAndValuesAsDictionary`
    - `GetEnumNamesAndValuesAsTuple`

```csharp
using static SatorImaging.UnitySourceGenerator.USGFullNameOf;  // usg<T>() to work

// 出力 -> Full.Namespace.To.MyClass.MyStruct.MyField
usg<MyClass>("MyStruct.MyField");

// リファクタリング完全対応版  fullnameof(...) が待たれる。。。
usg<MyClass>(nameof(MyClass.MyStruct), nameof(MyClass.MyStruct.MyField));

// インデント
sb.IndentChar(' ');  // デフォルト
sb.IndentSize(4);    // デフォルト
sb.IndentLevel(3);   // テンプレート初期設定
sb.IndentBegin();    // 字下げを一段上げる
{
    // int -> Enum のキャスト。
    // Enum はクラス内で作られがちで typeof(...).FullName が使えない。
    sb.IndentLine($"MyObject.EnumValue = ({usg<MyEnum>()})intValue");
    // --- or ---
    string CAST_MY_ENUM = "(" + usg(typeVariableCanBePassed) + ")";
    sb.IndentLine($"MyObject.EnumValue = {CAST_MY_ENUM}intValue");
}
sb.IndentEnd();  // 字下げを戻す
```



# 書き出し先フォルダー

書き出し先は以下の通り。生成フォルダーとターゲット・ジェネレータークラス名がファイル名に付与される。

- Assets/Scripts/<b>USG.g</b>/Test<b>.MinimalGenerator.g</b>.cs
- Assets/Scripts/<b>USG.g</b>/PanicMethod<b>.MethodGeneratorSample.PanicMethodGenerator.g</b>.cs

> 上記の例だとファイル名を編集しているので `Test.MinimalGenerator.g.cs_MyFirstTest.txt` が出てくる。



# ビルドイベント向けユーティリティー

`IPostprocessBuildWithReport` も実装しようかと思ったものの、ビルドイベントに勝手に処理を追加するのは訳わからん何故か動かん！の原因だし、BuildReport として渡される全てのファイル名を走査するのは効率も良くない。ということで。


```csharp
// search by class name if you don't know where it is.
var assetPath = USGUtility.GetAssetPathByName(nameof(MinimalGenerator));

// perform code generation.
USGEngine.IgnoreOverwriteSettingByAttribute = true;  // force overwrite
USGEngine.ProcessFile(assetPath);
```



# インターフェイス

理想はアトリビュートとインターフェイスによるフィルタリングですが、Unity 2021 は C# 9.0 で `abstract static` を含んだインターフェイスが使えません。

しょうがないのでメソッドのシグネチャを確認して存在しなければエラーをコンソールに出します。


![](https://dl.dropbox.com/s/kstbafbnyc54k2l/USG_IntefaceError.jpg)



## 命名規則

- ジェネレータークラスの名前はファイル名と一致
- ジェネレータクラスの名前はプロジェクト内で一意
- クラスが以下で始まる名前のアセンブリで宣言されている場合は対象としない
    - `Unity` (末尾ドット無し)
    - `System.`
    - `Mono.`



# インストール

- Latest: https://github.com/sator-imaging/Unity-AltSourceGenerator.git
- v1.0.0: https://github.com/sator-imaging/Unity-AltSourceGenerator.git#v1.0.0





# エディター拡張

手動でソースコード生成イベントの発火も可能です。「ジェネレーターのスクリプトファイル」か「生成されたファイル」を選択して、Project ウインドウで `Reimport` か `Unity Source Generator` 以下のメニューを実行。

ジェネレーターとして参照されているファイルを Reimport した場合は、関連するクラスすべてが再生成されます。`Force Generate...` はクラスアトリビュートの設定に関わらず強制的に上書き生成します。


![](https://dl.dropbox.com/s/skqa6mwh932lsrg/USG_FireEvent.jpg)



# TODO

ノンアロケーション中毒者向けに `UseCustomWriter` をアトリビュートに追加する。

ただ、Unity 2021 でやれることは全部やってるはず。`sb.ToString().AsSpan()` は Unity が `sb.GetChunks()` に対応したバージョンまで上げてくれないとどうしようもない（多分）

Unity 2021 より古いバージョンは `File.WriteAllText(...)`。2021 環境は便利でもう戻れない。2021 LTS は .NET Standard 2.0 ベースだけど 2.1 の一部機能だけバックポートした（らしい）のは Unity さんマジ大英断。

```csharp
using (var fs = new FileStream(context.OutputPath, FileMode.Create, FileAccess.Write))
{
    Span<byte> buffer = stackalloc byte[BUFFER_LENGTH];  // 61_440
    var span = sb.ToString().AsSpan();
    for (int i = 0; i < span.Length; i += BUFFER_MAX_CHAR_LENGTH)
    {
        var len = BUFFER_MAX_CHAR_LENGTH;
        if (len + i > span.Length) len = span.Length - i;

        int written = info.Attribute.OutputFileEncoding.GetBytes(span.Slice(i, len), buffer);
        fs.Write(buffer.Slice(0, written));
    }
    fs.Flush();
}
```

> `"..."u8` UTF-8文字列リテラルと `StringBuilder`→`FileStream` で無用の長物になるから要らない？？ そもそもソースジェネレーターはユーザーのデバイスじゃなくて開発機で実行されるから意味ないかなーー。




# おわりに

Roslyn のソースジェネレーターは○○という製品向けのソースジェネレーターを専門で作ってます。という事でもない限り使うのは厳しい印象。手間がちょっとね。

チュートリアルの通りにやればまあ動くんだけども、やってることは文字列リテラル延々と書くだけだし、必要なものと言えば名前空間とクラス名ぐらいのもんでしょう。あとは型から `System.Reflection` で手繰っていけばどうとでも。

Roslyn ソースジェネレーターを弄りつつ別の場所も弄る、がちょっと辛い。Unity で使えるバージョンがちょっと古いのもつらい。

Visual Studio で保存したときの更新イベントのフックがいまいちな時もあるけど、まあ満足。

--

以上です。お疲れ様でした。