Unity の IL2CPP と Managed Stripping High で壊れないために全パッケージへ link.xml を同梱した話
リフレクションを使わない設計でも JsonUtility の [Serializable] は消える。16 パッケージに link.xml を配り、実機検証で見つかった落とし穴をまとめる。
何に困っていたか
MimiCatHub は Steam 配布を想定しているので、最終的には IL2CPP と Managed Stripping Level High でビルドする。ところがこの構成は初期の設計から「継続課題」として残ったままで、Mono でしか動かしたことがなかった。
怖いのは、コードストリッピングが黙って壊すことである。コンパイルは通り、起動もする。しかしセーブデータの読み込みで一部のフィールドだけ既定値になっている、といった壊れ方をする。
何が危なくて、何が危なくなかったか
まず全パッケージを洗った。結論から言うと、危険はリフレクションではなく JsonUtility にあった。
- モジュールと Installer の解決は
McSettingsという ScriptableObject からの直接参照で行っており、ActivatorやType.GetTypeは使っていなかった。ここはストリッピングの影響を受けない。 - 一方、Save・Options・Analytics・Viewer・Diagnostics は
[Serializable]クラスをJsonUtilityで読み書きしている。コードから直接読まれないフィールドは、Highではリンカーが除去しうる。 RuntimeInitializeOnLoadMethodは 9 か所あったが、属性付きメソッドはリンカーが保持する。
つまり「リフレクションを使わない設計」が最大の対策になっていて、残る穴は JSON のフィールドだけ、と絞り込めた。
どう直したか
[Preserve] 属性を必要な型へ散りばめる案もあったが、漏れを人間が追うことになる。代わりに、全 16 パッケージの直下へ link.xml を置き、自分の Runtime Assembly を丸ごと保持することにした。
<linker>
<assembly fullname="MimiCatLab.MimiCatHub" preserve="all" />
</linker>
fullname はそのパッケージの Runtime/*.asmdef の name と一致させる。Editor と Tests の Assembly は Player に含まれないので書かない。
ゲーム側(Assets/ 配下)の型は、利用者が自分の link.xml で保持する。
<assembly fullname="Assembly-CSharp">
<type fullname="MyGame.SaveData" preserve="all" />
</assembly>
置いただけでは忘れるので、次の 3 点で機械的に守るようにした。
- 全パッケージに
link.xmlがあることをテスト(McLinkXmlTests)で検査する。 - 追加パッケージの雛形生成器が
link.xmlを最初から同梱する。 - ビルド Window に、Standalone のバックエンドと Stripping Level を表示し、
link.xmlが欠けたパッケージがあれば警告する。ビルド情報(McBuildInfo)にもバックエンドとストリッピングを焼き込み、起動ログの末尾に出す。
版=0.29.0 ... バックエンド=IL2CPP ストリッピング=High
実機で何が起きたか
2026 年 9 月 11 日に IL2CPP の実機検証を行った。結果は合格だが、途中の躓きの方が記事にする価値がある。
Unity Hub がモジュール追加を出さない。 Hub が同じバージョンの Editor を 2 か所(C: と D:)で検出し、片方を「located」扱いにしたため「モジュールを加える」ボタンが出なかった。C: 側を消してから、コマンドラインで導入した。
"Unity Hub.exe" -- --headless install-modules --version 6000.4.0f1 --module windows-il2cpp
バックエンド切替時にビルドが落ちる。 出力先に Mono の成果物が残っていると次のエラーで止まる。出力フォルダーを空にして再実行したら通った。
BuildFailedException: Build path contains a project previously built with the Mono2x scripting backend
JSON の往復は数値で確認した。 起動して保存し、強制終了して再起動する。1 回目は before=-1 saved=1.5、再起動後は before=1.5 saved=2.5 readback=2.5 で、High でも値が欠落しなかった。
IL2CPP ではないクラッシュを踏んだ。 ウィンドウ付きで 2 回目以降に起動すると、Addressables の初期化中に 0xc0000005 で落ちた。イベントログとクラッシュダンプを見ると、落ちているのは GameAssembly.dll ではなく Windows 側の UIAutomationCore.DLL だった。-batchmode -nographics では完走する。これは未解決で、Mono ビルドでも再現するかを切り分ける予定である。
教訓
- リフレクションを使わない設計そのものが最大のストリッピング対策になる。 危険箇所を
JsonUtilityの[Serializable]だけに絞れた。 [Preserve]を散らすよりpreserve="all"のlink.xmlを 1 枚。 パッケージ単位で漏れがなく、テストで検知できる。- 規約は文書ではなくテストと生成器に落とす。 新しいパッケージが自動的に守る形にする。
- バックエンド切替時は出力フォルダーを空にする。 設計の問題ではなく手順の問題なので、文書に残す。
- クラッシュの責任範囲を先に切り分ける。 どの DLL が落ちたかを見るまで IL2CPP のせいと決めつけない。
対象外にしたこと
外部依存 Assembly(Steamworks.NET・Newtonsoft.Json・Unity Localization など)の保持、IL2CPP のジェネリック共有や AOT の問題、ビルド Window からの設定変更は今回の範囲に入れていない。link.xml の追加はビルド時の保持指示であって API の変更ではないので、各パッケージの版は上げていない。
Related Articles