CORETECH ENGINEER BLOG

株式会社サイバーエージェント SGEコア技術本部 技術ブログ

ILPostProcessor 入門 第5回目「様々なテクニック」

この記事は ILPostProcessor 入門シリーズの第5回です。

今回は、ILPostProcessorの開発を助けるツールや資料などを紹介していきます。

デバッガのアタッチ

第二回で紹介した通り、Unity の ILPostProcessor は Unityと独立したプロセスで動いています。そのプロセスは普通の .NET プロセスなので、デバッガをアタッチし、ブレークポイントを張りながらデバッグすることができます。

今回は JetBrains Rider で説明しますが、他のIDEでも基本的には同じです。

💡 今回はUnity 2022.3 を使用している前提で説明します。

デバッガの構成

「外部ソースのデバッグを可能にする」を有効化します。

💡 Visual StudioVisual Studio Codeでは「Just My Code」を無効化する操作に相当します。

プロセスにアタッチ

右上のメニューから「プロセスにアタッチ」を選択します。

プロセスの一覧から Unity.ILPP.Runner を選択してアタッチします。

💡 Unity.ILPP.Runner は立ち上げている Unity のインスタンスごとに作成されます。

デバッガが起動したら、適当な箇所にブレークポイントを設定し、Unity側でコンパイルを走らせます。

これでデバッグできるようになりました。

便利なツール

SharpLab

このシリーズでも何度か登場した、C#コンパイル結果を表示することができる Web ツールです。

コンパイル結果は IL としても表示できますし、それを逆コンパイルした低レベルな C# としても表示できます。また、Web 上でコードの実行まで行うことができ、細かい挙動の確認にも便利です。

ILSpy

C# を逆コンパイルできるデスクトップツールです。ローカルの DLL を逆コンパイルしたり、IL を見たりするのに使えます。Unity の場合、Library/ScirptAssemblies に ILPostProcessor 適用後の DLL が置かれるので、主にはこれを読み込ませます。

基本的には Windows 専用ですが、macOS / Linux 向けには Avalonia UI を使用した移植版が公開されています。動作は少し不安定ですがこちらも使用可能です。

コンパイルに失敗する場合は、たいていIL生成を何か間違えています。

JetBrains Rider

JetBrains Rider を使っている方は、IL Viewer を使用して IL を確認したり、デコンパイルを行うことができます。IDE 上で完結するのでとても便利です。

Unity では、対象の Unity プロジェクトを開いているワークスペースでは IL Viewer が動かないという問題がありますが、別のワークスペースの IL Viewer で直接 DLL を読み込むと回避できます。

ildasm

DLL を IL のテキスト表現に変換できる CLI ツールです。ほとんどのケースでは ILSpy や Rider で同じことができますが、ildasm ではより細かい情報(バイナリフォーマットレベルの情報など)も出力できます。また、DLL を丸ごと IL テキスト表現化して diff をとってデバッグする、みたいな用途にも使えます。

Visual Studio を使っている方は、付属の開発者コマンドプロンプトから使用できます。

macOS / Linux 向けには NuGet パッケージとして配布されているバイナリが使用可能です。

NuGet パッケージを手動でダウンロードして ZIP ファイルとして展開すると、runtimes フォルダ下にバイナリがあります。これにPATHを通しておくといいでしょう。

💡 筆者の macOS 環境では、オプションを指定する際にスラッシュの代わりにハイフンを使用しないとうまく動作しませんでした。
ildasm Assembly-CSharp.dll -out=Assembly-CSharp.dll.il

ILVerify

DLL (IL) のエラーチェックを行える CLI ツールです。.NET ツールとしてインストールできます。

dotnet tool install --global dotnet-ilverify
ilverify <input-file-path>... [options]

-r オプションで、依存するすべてのアセンブリのパスを指定する必要があります。足りないアセンブリはエラーとして表示してくれるので、実行しながら適宜追加していくといいでしょう。必要なアセンブリのパスは Unity が生成する csproj の中から取ってこれます。

エラーがなければ下のようなメッセージが表示されます。

All Classes and Methods in (パス) Verified

トラブルシューティング

ILPostProcessor の実装でよく遭遇するエラーとその対処法を紹介します。

Member '~~~' is declared in another module and needs to be imported

第4回で説明した ImportReference() が不足している時に表示されます。

TypeLoadException

型のメタデータに何らかの不正がある場合に表示されることが多いです。

InvalidProgramException

IL 命令、特にスタックの状態に不整合がある場合に発生することが多いです。一旦 ilverify をかけてみましょう。

よくある落とし穴として、IL では特定の命令時点におけるスタックの深さと各スタック値の型は、コードパスによらず同じである必要があります。例えば以下のように、ループしながら漸次スタックを積む、みたいなコードは仕様で禁止されています。

// (ダメな例)

// ループカウンタ初期化 (i = 0)
ldc.i4.0
stloc.0

// ループ内 (ここで、イテレーションごとにスタックの深さが一定にならないのでエラー)
LOOP: 

// ループ条件 (i < 4)
ldloc.0
ldc.i4.4
bge.s END

// 1を積む
ldc.i4.1

// ループカウンタを加算 (i++)
ldloc.0
ldc.i4.1
add
stloc.0
br.s LOOP

// ループ終了
END:

// この時点で1を4つ積みたい

こうした場合は、ループを使わずにスタックを積んでください。

// (良い例)

ldc.i4.1
ldc.i4.1
ldc.i4.1
ldc.i4.1

MissingMethodException, MissingFieldException

メソッド参照やフィールド参照、アセンブリ参照に誤りがあることが多いです。

実行時のクラッシュ

不正なメモリアクセスなどが疑われます。意図せず unsafe なコードになっていないか、特に値と参照を間違えていることがよくあるので注意しましょう。

IL2CPP ビルドの失敗

IL2CPP はビルド時に IL を解析しています。IL2CPP ビルドに失敗する場合、InvalidProgramException と同様に、スタックの状態に不整合がある場合が多いです。

付録:資料集

ECMA-335

.NETランタイムの仕様書です。最も詳細で厳密な.NETランタイムの挙動が記載されており、最終的にはこれを確認すれば大体のことが分かります。

ECMA-335 CLI Specification Addendum

ECMA-335 の補足事項です。ECMA-335 の最終更新は2012年ですが、.NET Core 以降のランタイム実装において、仕様書と異なる部分などについて記載されています。

System.Reflection.Emit.OpCodes

動的 IL 生成で使用される OpCodes クラスのドキュメントで、各IL命令の挙動が記載されています。ECMA-335 ほど詳細ではありませんが、コンパクトで読みやすいです。

Mono.Cecil Wiki

Mono.Cecil に関して、ほぼ唯一の公式ドキュメントです。正直あまり情報は多くないので、Mono.Cecil に関してはソースコードを読まないとわからないことが多いです。

その他、記事など