Home

Awesome

<!-- omit in toc -->

Alternative Source Generator for Unity

Alternative Source Generator is built on the Unity native functions.

As you already know, Roslyn's source generator is too sophisticated. This framework provides more simple, ease of use and good enough functions for source code generation.

<p><details lang="ja" --open><summary><small>日本語 / JA</small></summary>

超簡単に使える Unity 向けソースジェネレーターです。

<!------- End of Details JA Tag -------></details></p> <p><details lang="en" --open><summary>📃 Table of Contents</summary> <!------- End of Details EN Tag -------></details></p>

How to Use

Here is minimal implementation of source generator.

See API Reference for further details.

<p><details lang="ja" --open><summary><small>日本語 / JA</small></summary>

最小限のソースジェネレーターの構成はこちら。StringBuilder が渡されるので書き込んで true を返せば context.OutputPath に内容を書き込みます。false を返せば書き込みを中止できます。

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

Method Generator

This example will add Panic() method to target class.

<p><details lang="ja" --open><summary><small>日本語 / JA</small></summary>

ターゲットのクラスに Panic() メソッドを追加するサンプル。

<!------- End of Details JA Tag -------></details></p>
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.

        // code generation
        sb.Append($@"
namespace {context.TargetClass.Namespace}
{{
    internal partial class {context.TargetClass.Name}
    {{
        public void Panic() => throw new System.Exception();
    }}
}}
");
        return true;
    }
}

How to Generate Source Code

using SatorImaging.UnitySourceGenerator;

namespace Sample
{
    // Add attribute to target class to use method generator.
    // Note that class must be defined as partial class.
    [UnitySourceGenerator(typeof(PanicMethodGenerator), OverwriteIfFileExists = false)]
    internal partial class MethodGeneratorSample
    {
    }

}

Result

Generated code looks like this.

// <auto-generated>PanicMethodGenerator</auto-generated>

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

Self-Emit Generator

Here is target-less, self-emitting generator example.

It is useful to generate static database that cannot be generated on Unity runtime. For example, build asset GUIDs database using UnityEditor.AssetDatabase, resource integrity tables, etc.

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;
    }
}

Result

// <auto-generated>MinimalGenerator</auto-generated>
Asset Path: Assets/Scripts/MinimalGenerator.cs
Hello World from Sample.MinimalGenerator

Output Directory and File Name

Source Generator creates USG.g folder next to target script and append class names to file name.

Resulting file path will be:

<p><details lang="ja" --open><summary><small>日本語 / JA</small></summary>

書き出し先は上記の通り。フォルダーとターゲット・ジェネレータークラス名が付与されます。

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

NOTE: In above example, output path is modified so that resulting file name is Test.MinimalGenerator.g.cs_MyFirstTest.txt

Coding Goodies

There are utility methods for coding source generator more efficient and readable.

// indent utility
sb.IndentChar(' ');  // default
sb.IndentSize(4);    // default
sb.IndentLevel(3);   // template default
sb.IndentBegin("void MethodName() {");
{
    // cast int value to enum
    sb.IndentLine($"MyObject.EnumValue = ({usg<MyEnum>()})intValue");
    // --- or ---
    string CAST_MY_ENUM = "(" + usg(targetTypeVar) + ")";
    sb.IndentLine($"MyObject.EnumValue = {CAST_MY_ENUM}intValue");
}
sb.IndentEnd("}");

usg is a special utility that is designed for refactoring-ready source generator more readable, script template import it as a static library by default.

<p><details lang="ja" --open><summary><small>日本語 / JA</small></summary>

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

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

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

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

<!------- End of Details JA Tag -------></details></p>
using static SatorImaging.UnitySourceGenerator.USGFullNameOf;  // usg<T>() to work

// usg<T>() allows to write refactoring-ready code with auto completion
sb.Append($"public static {usg<Dictionary<int, float>>()} MyDict = new() {{ init... }};");

// usg<T>{params string[]) to generate full name with specified member name.
usg<MyClass>("NestedStruct.MyField");  // -> global::Full.Namespace.To.MyClass.MyStruct.MyField
// most strict refactoring-ready code
usg<MyClass>(nameof(MyClass.NestedStruct), nameof(MyClass.NestedStruct.MyField));

// usg(object valueOrType, bool isFullName) to retrieve full type definition literal
static class MyClass {
    Dictionary<int, List<Dictionary<string, float[][]>[]>> Complex = new(0);  // usg() throws when null
}
usg(MyClass.Complex);  // -> global::...Dictionary<int, global::...List<global::...Dictionary<string, float[][]>[]>>

Samples

SceneBuildIndexGenerator

This sample allows you to handle scene index more efficiently.

To use this sample, add [UnitySourceGenerator(typeof(SceneBuildIndexGenerator))] attribute to your class. after that, you can use the following enum and helper methods.

Utility Functions for Build Event

There are utility functions to perform source code generation on build event.

<p><details lang="ja" --open><summary><small>日本語 / JA</small></summary>

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

<!------- End of Details JA Tag -------></details></p>
// generate code for specified generator type
USGUtility.ForceGenerateByType(typeof(MinimalGenerator));

// or for the emitter type
USGUtility.ForceGenerateByType(typeof(ClassHasUSGAttribute));

Technical Notes

As of C# 9.0, it doesn't allow to define abstract static methods in interface, USG reports error when source generator class doesn't implement required static methods.

<p><details lang="ja" --open><summary><small>日本語 / JA</small></summary>

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

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

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

Naming Convention

<p><details lang="ja" --open><summary><small>日本語 / JA</small></summary> <!------- End of Details JA Tag -------></details></p>

<auto-generated/> Tag

USG automatically adds document tag at the beginning of generated file. You can remove this document tag by sb.Clear() in Emit() method.

<p><details lang="ja" --open><summary><small>日本語 / JA</small></summary>

渡される StringBuilder の冒頭にはドキュメントタグが入ってます。不要なら sb.Clear() してください。

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

Unity Editor Integration

Installation

Use the following git URL in Unity Package Manager (UPM).

USG Control Panel & Window

Context Menu

<p><details lang="ja" --open><summary><small>日本語 / JA</small></summary>

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

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

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

There is an ability to invoke source code generation by hand. With generator script file or generated file selected in Project window:

Troubleshooting

Generator script update is not applied to generated file.

Usually, this problem happens when Unity automatically reloads updated scripts WITHOUT Editor window getting focus. To solve the problem:

  1. Close Unity and Visual Studio.
  2. Restart Unity.
  3. Launch Visual Studio by double clicking .cs script in Unity Editor.

NOTE: There is experimental feature to suspend auto reloading while Unity Editor in background (doesn't get focused). Open Edit > Project Settings > Alternative Source Generator to enable it.

Copyright

Copyright © 2023-2024 Sator Imaging, all rights reserved.

License

<p> <details> <summary>The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.</summary>
MIT License

Copyright (c) 2023-2024 Sator Imaging

Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:

The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.

THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
</details> </p>

 
 

Devnote

TODO

<!-- useless - Add new attribute option `UseCustomWriter` to use it's own file writer instead of builtin writer. For the "non-allocation" addicted developers. - `USGEngine.ProcessingFile()` doesn't care what happens in custom writer. just returns true in this situation. - Option is for generator class. Referenced generator class doesn't have `UnitySourceGenerator` attribute so that need to retrieve it from target classes. (how handle conflicts?) - `USGContext.UseCustomWriter` can be used to prevent writing file but `StringBuilder` is built prior to `Emit()` method. --> <!-- Clarify terminology of generator types, especially in source code comments. currently, referenced/referencing generator, or just generator, confusing! - Generator class: - Class has `Emit()` method to generate code. And does NOT have `UnitySourceGenerator` attribute. - current -> "referenced" generator, "referenced only" generator, etc. - Target, or Emitter class: - Class has `UnitySourceGenerator` attribute to invoke code generation. And does NOT have `Emit()` method. - current -> "referencing" class, just generator, etc. - Self-Emit Generator (Self-Generator) class: - Class has both `Emit()` method and `UnitySourceGenerator` attribute. Works only a file. No dependencies. -->