【C#】Source Generator と Serializable のトラップ
Source Generator (たぶんAnalyzerでもそう) で、特定のクラスが[Serializable]
かを判定したいとき、真っ先に思いつくのはこんな方法だと思う。
public static bool IsSerializable(ITypeSymbol symbol)
{
// SerializableAttributeのシンボルを取得
var serializableAttributeSymbol = compilation.GetTypeByMetadataName("System.SerializableAttribute");
// GetAttributes()でマッチするAttributeがあるか検索
return symbol.GetAttributes().Any(a => SymbolEqualityComparer.Default.Equals(a.AttributeClass, serializableAttributeSymbol));
}
これでは正しく動かない場合がある。
今回自分がハマったのは、Unityで動かした場合だった。asmdefで切り出したアセンブリに[Serializable]
なクラスを作成し、そのシンボルに対してメインアセンブリのSource GeneratorからIsSerializable()
を行ったところ、UnityとIDE上でSource Generatorの結果が一致しなくなった。Unity上ではfalse
を返し、IDE上ではtrue
を返す。
実は、[Serializable]
はIL上では特殊な扱いをされており、ほかのAttributeと異なる表現をされる。
using System;
[Custom]
[Serializable]
public class C
{
}
public class CustomAttribute : Attribute
{
}
こういうコードを書くと、こんなILになる。
.class public auto ansi serializable beforefieldinit C
extends [System.Runtime]System.Object
{
.custom instance void CustomAttribute::.ctor() = (
01 00 00 00
)
(略)
} // end of class C
CustomAttribute
とSerializable
で扱いが異なるのがわかる。Serializable
は他の属性とは異なり、クラス自体の宣言にserializable
という専用の属性を付与して表現される。
これがなぜUnityとIDEでの結果の食い違いに至るのかというと、IDE上ではおそらくソリューション単位でMSBuildのビルドパイプラインを走らせているのに対し、Unity上ではひとつひとつのアセンブリを個別にコンパイルしていることが原因じゃないかと思う。
IDE上ではソリューションをまるごと食わせているので、asmdef側をコンパイルした際に生成されたSemanticModelをメインアセンブリのコンパイルでも利用することができ、そこにはGetAttribute()
で取得できるカスタム属性としてのSerializable
の情報が含まれている。対してUnity上ではメインアセンブリのコンパイル時にはasmdefは既にDLLになっており、DLLからSemanticModelを再構成している。そしてそこにはカスタム属性としてのSerializable
の情報は含まれていない。
それでは、カスタム属性ではなく、ILでクラスに直接付与されていたserializable
に相当する情報を得られるAPIを利用できればよさそう。ということで、実は下のコードでオッケー。
public static bool IsSerializable(ITypeSymbol symbol)
{
return symbol is INamedTypeSymbol { IsSerializable: true };
}
GetAttributes()
という回りくどい方法を使うと、環境によって動いたり動かなかったりするために余計にハマってしまった。
UnityにCoreCLRが到来するにつれ、ビルドパイプラインもMSBuildに寄せていく計画らしいけど、こういう細かい差異もなくなっていくとうれしいな~