Open1

【C#】Source Generator と Serializable のトラップ

rucchoruccho

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

https://sharplab.io/#v2:C4LglgNgNAJiDUAfAAgJgIwFgBQODaAwgK4DOwA9gLYC6+AygKYBOYAhhGAF6sBGEDtbMgDMAAjSiConAG8cAXxw4R41JNIVKAQWDAWPIsAaiQonXrAGjshUA===

CustomAttributeSerializableで扱いが異なるのがわかる。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に寄せていく計画らしいけど、こういう細かい差異もなくなっていくとうれしいな~