🖋️

Google.ProtobufのTypeInitializationExceptionの対処方

2024/12/05に公開

はじめに

こんにちは。
アプリボットアドベントカレンダー 3日目の記事です。

本記事では、UnityのManagedCodeStrippingとGoogle.Protobufの共存についてお話しします。

簡単に

UnityのGoogle.Protobufライブラリ内で TypeInitializationException が発生しました。
自動生成したコードが消えてしまっていたので、link.xmlなどで自動生成コードを保護してあげることで解決しました。

前提

ManagedCodeStripping

まずManagedCodeStrippingについてです。Unityがビルドを行う際に、アプリケーション側から参照をされていないコードを削除することで、アプリケーションのサイズを削減する仕組みのことです。
詳しい仕組みやコード削除の回避については、公式ドキュメントや昨年の私の記事を参照してください。
Unity公式 マネージドコードストリッピング
IUnityLinkerProcessorでCodeStrippingからコードを守る

Google.Protobuf

protobufはデータ構造のシリアライズ、デシリアライズを効率的に行う仕組みです。言語を跨いだ通信の仕組みを利用することができます。
弊プロジェクトではサーバーをGo言語で作成しています。通信時に使用するコードをGoogle.Protobufを用いてC#コードの自動生成を行っています。

事象

ある日Androidにて動作確認を行っていた際に、このようなエラーが発生しました。

[Fatal] Unhandled exception: System.TypeInitializationException: The type initializer for `プロジェクトのコード` threw an exception.
...
System.ArgumentException: Not all required properties/methods are available

直訳すると 必要なプロパティ/メソッドがすべて利用できるわけではないです とのこと。
スタックトレースを見ても

Google.Protobuf.Reflection.SingleFieldAccessor..ctor (System.Reflection.PropertyInfo property, Google.Protobuf.Reflection.FieldDescriptor descriptor) (at <00000000000000000000000000000000>:0)
Google.Protobuf.Reflection.FieldDescriptor.CreateAccessor () (at <00000000000000000000000000000000>:0)
Google.Protobuf.Reflection.FieldDescriptor.CrossLink () (at <00000000000000000000000000000000>:0)
Google.Protobuf.Reflection.MessageDescriptor.CrossLink () (at <00000000000000000000000000000000>:0)
Google.Protobuf.Reflection.MessageDescriptor.CrossLink () (at <00000000000000000000000000000000>:0)
Google.Protobuf.Reflection.FileDescriptor.CrossLink () (at <00000000000000000000000000000000>:0)
Google.Protobuf.Reflection.FileDescriptor.BuildFrom (Google.Protobuf.ByteString descriptorData, Google.Protobuf.Reflection.FileDescriptorProto proto, Google.Protobuf.Reflection.FileDescriptor[] dependencies, System.Boolean allowUnknownDependencies, Google.Protobuf.Reflection.GeneratedClrTypeInfo generatedCodeInfo) (at <00000000000000000000000000000000>:0)
Google.Protobuf.Reflection.FileDescriptor.FromGeneratedCode (System.Byte[] descriptorData, Google.Protobuf.Reflection.FileDescriptor[] dependencies, Google.Protobuf.Reflection.GeneratedClrTypeInfo generatedCodeInfo) (at <00000000000000000000000000000000>:0)
Google.Protobuf.Reflection.DescriptorReflection..cctor () (at <00000000000000000000000000000000>:0)
以下プロジェクトコード
...

Google.Protobufのコードで例外が出ているので、プロジェクトコードが直接悪いわけではなさそう。

TypeInitializationException はstaticクラスの初期化を行う際に処理が失敗することで発生します。
ので、直接的な原因を探るためにGoogle.Protobufの中身を見に行きます。

原因究明 / Google.Protobufの仕組み

Google.Protobufは通信で送られてきた byte配列を MessageParser<T> を使用してC#インスタンスにパースします。
各種自動生成されたクラスは public static pb::MessageParser<ApiResponse> Parser を持っています。

パースを行う際に MessageDescriptor などの、型やプロパディの情報を持つ情報群を使用しています。
DescriptorはMono.Cecilなどで使用されているクラスと構造が似ています。C#のdll内部を解析した後の情報が格納されているイメージです。
これらDescriptorをstaticコンストラクタで生成しています。今回の場合、SingleFieldAccessor。つまり特定のフィールドアクセスを行うための情報をコンストラクタにて作成する際に例外を吐いていました。

SingleFieldAccessor の中身を見ると

this.setValueDelegate = property.CanWrite ?
ReflectionUtil.CreateActionIMessageObject(property.GetSetMethod()) :
throw new ArgumentException("Not all required properties/methods available");   

このように、Setterが存在する前提で組まれています。
他にも Getterが存在しているか、Clearメソッドが存在しているかをチェックし、これらがない場合も ArgumentException を発生させます。

この時点のPropertyInfoの情報が欠けているらしいです。
では、このPropertyInfoはどこから生まれたものなのか。
遡ると、 FieldDescriptorに

PropertyInfo property = this.ContainingType.ClrType.GetProperty(this.propertyName);

とあるので、FieldDescriptorのContainingTypeの出所を探ると、
FileDescriptor.FromGeneratedCodeに辿り着きます。
つまり GeneratedClrTypeInfo のインスタンス作成に必要な System.Type clrType の中身がそのまま PropertyInfo として使用されています。
GeneratedClrTypeInfo は型本体の情報に加え、Parserやプロパティの名前などを持ちます。

この時点で、消えているのはプロジェクト側で自動生成したコード だと言うことが判明しました。
確かにアプリケーション側で参照されていないコードはUnityのManagedCodeStrippingの影響で消えてしまうので、link.xmlに含めるなどを行い保護することが重要です。

結論

Protobufを用いて自動生成したコード、リフレクション参照されるコードは Strippingから保護をする。
理想としては、自動生成したコードの中でアクセスするものだけを残すことですが、原因のプロパティを見つけ出し保護をすることは大変労力がかかります。アプリケーションサイズ削減の文脈ではかなり非効率ですが、Protobufが関わっている部分は保護することが安定択だと思います。
以上、ProtobufとCodeStrippingが合わさって発生するエラーの解決法でした。

Discussion