概要
UnityでSourceGeneratorが話題になってるものの、公式サイト等では Visual Studio での作成方法ばかりだったのでmacやLinuxで作るべくdotnet
コマンドを使って実装してみました。
(つまり公式の手順のうちDLLのプロジェクト作成とビルドをdotnet
コマンドでやっただけ)
その時の手順メモです。
さっと知りたい人向けに公式サイトとの違い
以下の手順を置き換えるだけでした。
Visual Studioで、.NET Standard 2.0 をターゲットとする .NET 標準ライブラリプロジェクトを作成します。
dotnet new classlib -lang "C#" -f "netstandard2.0" -o "MyGenerateAttribute"
Microsoft.CodeAnalysis NuGet パッケージをインストールします。
dotnet add package "Microsoft.CodeAnalysis.CSharp" --version "3.8.0"
作成方法(フルバージョン)
# プロジェクト新規作成
dotnet new classlib -lang "C#" -f "netstandard2.0" -o "MyGenerateAttribute"
# 必要ならソリューションを作成
dotnet new sln
dotnet sln add MyGenerateAttribute/MyGenerateAttribute.csproj
# 必要なパッケージをインストール
cd MyGenerateAttribute
dotnet add package "Microsoft.CodeAnalysis.CSharp" --version "3.8.0"
# コード書いたらビルド
dotnet build -c Release
cp bin/Release/netstandard2.0/*.dll /path/to/UnityProject/Assets/Plugins
上記で配置し終わったらUnityで配置したDLLのインスペクタを開いて
-
Select platforms for plugin
からAny Platform
のチェックを外す -
Include Platforms
から全てのチェックを外す -
Asset Labels
をRoslynAnalyzer
に設定する
とすれば準備は完了です。
あとはUnityで通常通りコードを書くだけです。
今回作成したコード
とりあえず目標としてはフィールドに設定することでGetプロパティを生成するSourceGeneratorを作りたいと思います。
理由は
- 既存コードからテンプレート的にコードを生成するための最低限の解析手順が踏めそう
- 生成自体も分岐が少なく簡単に出力できそう
という点で、公式サンプルから解析部分を読み取ってその結果を使って何かする、というのにちょうど良さそうな大きさだったからです。
名前とか、細かい部分は適当なので「このパターンのコードだとエラーになる」とかあるので適宜修正してください...
SourceGenerator のコード
using System.Collections.Generic;
using System.Linq;
using System.Text;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
namespace MyLib
{
[Generator]
public class MyGenerator : ISourceGenerator
{
// Attributeのコード
private const string ATTRIBUTE_CODE =@"
namespace MyGenerateAttributeNS {
/// <summary>
/// フィールドのGetプロパティを生成する
/// </summary>
[System.AttributeUsage(System.AttributeTargets.Field, AllowMultiple = false)]
internal sealed class MySourceGeneratorAttribute : System.Attribute
{
/// <summary>
/// プレフィックス
/// </summary>
public string Prefix { get; set; }
/// <summary>
/// フィールドのGetプロパティ生成
/// </summary>
public MySourceGeneratorAttribute() {
}
}
}";
public void Initialize(GeneratorInitializationContext context)
{
context.RegisterForSyntaxNotifications(() => new MySyntaxReceiver());
}
public void Execute(GeneratorExecutionContext context)
{
// Attribute のソースコードを追加
context.AddSource("MySourceGeneratorAttribute.g.cs", ATTRIBUTE_CODE);
if (!(context.SyntaxReceiver is MySyntaxReceiver syntaxReceiver))
return;
// Attribute のソースコードを解析
CSharpParseOptions options = (context.Compilation as CSharpCompilation)
.SyntaxTrees
.First()
.Options as CSharpParseOptions;
var compilation = context.Compilation.AddSyntaxTrees(CSharpSyntaxTree.ParseText(SourceText.From(ATTRIBUTE_CODE, encoding: Encoding.UTF8), options));
// SourceGenerator のAttributeが付与されたシンボルを取り出す
var attributeSymbol = compilation.GetTypeByMetadataName("MyGenerateAttributeNS.MySourceGeneratorAttribute");
var fieldSymbols = new List<IFieldSymbol>();
foreach (var field in syntaxReceiver.CandidateFields)
{
var model = compilation.GetSemanticModel(field.SyntaxTree);
foreach (var variable in field.Declaration.Variables)
{
var fieldSymbol = model.GetDeclaredSymbol(variable) as IFieldSymbol;
if (fieldSymbol.GetAttributes().Any(att => att.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default)))
{
fieldSymbols.Add(fieldSymbol);
}
}
}
// 実際にAttributeが付与されたソースコードから新しいソースコードを作成する
foreach (var group in fieldSymbols.GroupBy(f => f.ContainingType))
{
var code = MakeGetPropertyCode(group.Key, group.ToArray(), attributeSymbol);
context.AddSource($"{group.Key}__MySourceGeneratorAttribute.g.cs", code);
}
}
private string MakeGetPropertyCode(INamedTypeSymbol classSymbol, IFieldSymbol[] fieldSymbols, ISymbol attributeSymbol)
{
if (!classSymbol.ContainingSymbol.Equals(classSymbol.ContainingNamespace, SymbolEqualityComparer.Default))
{
return null;
}
var namespaceString = classSymbol.ContainingNamespace.ToDisplayString();
var codeBuilder = new StringBuilder();
codeBuilder.AppendLine($"namespace {namespaceString} {{");
codeBuilder.AppendLine($"public partial class {classSymbol.Name} {{");
foreach (var fieldSymbol in fieldSymbols)
{
var fieldName = fieldSymbol.Name;
var fieldType = fieldSymbol.Type;
var attributeType = fieldSymbol.GetAttributes().Single(ad => ad.AttributeClass.Equals(attributeSymbol, SymbolEqualityComparer.Default));
var prefixConstruct = attributeType.NamedArguments.Single(keyValue => keyValue.Key == "Prefix").Value;
if (prefixConstruct.IsNull)
{
continue;
}
var prefix = prefixConstruct.Value?.ToString();
codeBuilder.AppendLine($"public {fieldType.ToDisplayString()} {prefix}{fieldName.Substring(0, 1).ToUpper()}{fieldName.Substring(1)} => {fieldName};");
}
codeBuilder.AppendLine("}");
codeBuilder.AppendLine("}");
return codeBuilder.ToString();
}
internal class MySyntaxReceiver : ISyntaxReceiver
{
public List<FieldDeclarationSyntax> CandidateFields = new List<FieldDeclarationSyntax>();
public void OnVisitSyntaxNode(SyntaxNode syntaxNode)
{
if (!(syntaxNode is FieldDeclarationSyntax fieldDecSyntax))
return;
if (fieldDecSyntax.AttributeLists.Count <= 0)
return;
this.CandidateFields.Add(fieldDecSyntax);
}
}
}
}
Unity側のコード
namespace Data {
public partial class UserData
{
[MyGenerateAttributeNS.MySourceGenerator(Prefix = "")]
private int id;
[MyGenerateAttributeNS.MySourceGenerator(Prefix = "User")]
private string name;
public UserData(int id, string name) {
this.id = id;
this.name = name;
}
}
}
using UnityEngine;
public class MainScene : MonoBehaviour
{
// Start is called before the first frame update
void Start()
{
var userData = new Data.UserData(1, "Taro");
Debug.Log($"Generated Property: {userData.Id} {userData.UserName}");
}
}
作成のポイント
Unityドキュメントにも書いてありますがバージョン指定などがされているのでコマンド実行時に指定を忘れないようにするのがポイントです。
- .NET のバージョン
- .NET Standard 2.0
dotnet new classlib -lang "C#" -f "netstandard2.0"
- .NET Standard 2.0
- Microsoft.CodeAnalysis のバージョン
- Microsoft.CodeAnalysis 3.8
dotnet add package "Microsoft.CodeAnalysis.CSharp" --version "3.8.0"
- Microsoft.CodeAnalysis 3.8
また、これに伴ってMicrosoftのSourceGeneratorサンプルも16.7
のtagを参照するのが良さそうです。
main
ブランチの最新だといくつかMicrosoft.CodeAnalysis.CSharp 3.8
ではまだ使えないメソッドがありました。
参考
Unity マニュアルのページ:
https://docs.unity3d.com/ja/2022.3/Manual/roslyn-analyzers.html
Microsoft の SourceGenerator サンプル:
https://github.com/dotnet/roslyn-sdk/tree/16.7/samples/CSharp/SourceGenerators/SourceGeneratorSamples