【C#】InvalidProgramExceptionエラーの原因と対処法まとめ
InvalidProgramException
はCLRが壊れたILや不正なメタデータを検出した際に発生する例外です。
ビルド設定のx86/x64不一致、破損DLL、動的コード生成バグなどが主因となるため、プラットフォーム統一、参照DLLの置き直し、クリーン再ビルド、最新コンパイラ適用で多くは解決します。
InvalidProgramExceptionとは
例外の定義と特徴
InvalidProgramException
は、C# のプログラム実行中に発生する例外の一つで、主に実行時にプログラムの中身が無効であると共通言語ランタイム(CLR)が判断した場合にスローされます。
具体的には、コンパイルされた中間言語(IL)が正しくない、または破損している場合に発生します。
この例外は、通常のコードのバグとは異なり、ILコードの構造的な問題や、JITコンパイラが処理できない不正な命令が含まれている場合に起こります。
たとえば、メソッドのILが不正な形式であったり、メタデータが壊れている場合などが該当します。
特徴としては、以下の点が挙げられます。
- 実行時に発生し、通常の例外処理で捕捉されることが多い
- プログラムのビルドやコンパイル段階での問題が原因であることが多い
- 例外メッセージには「プログラムが無効です」や「ILが不正です」といった内容が含まれることが多い
- 通常のコードロジックエラーとは異なり、ILコードやアセンブリの整合性に起因する
この例外が発生すると、プログラムの実行が停止し、原因を特定しない限り正常に動作しません。
したがって、発生した場合はビルド設定や依存関係、ILコードの生成過程を重点的に調査する必要があります。
BadImageFormatExceptionとの違い
InvalidProgramException
と似た例外に BadImageFormatException
があります。
両者はどちらもプログラムの実行時に発生し、アセンブリやILコードの問題を示しますが、原因や発生状況に違いがあります。
例外名 | 発生原因の概要 | 発生タイミング | 主な特徴・違い |
---|---|---|---|
InvalidProgramException | ILコードが不正、またはJITコンパイラが処理できない無効なプログラムが存在する場合 | 実行時(JITコンパイル時) | ILの構造的な問題やメソッドの不正なIL命令が原因。プログラムの中身が無効であることを示します。 |
BadImageFormatException | アセンブリのフォーマットが不正、またはプラットフォームの不一致(例:x86とx64) | 実行時(アセンブリ読み込み時) | アセンブリファイル自体が破損しているか、ビット数の不一致などで読み込めない場合に発生。 |
BadImageFormatException
は、主にアセンブリのファイル形式やプラットフォームの不整合に起因し、CLRがアセンブリを読み込めない場合に発生します。
一方で、InvalidProgramException
はアセンブリは読み込めているものの、その中のILコードが不正である場合に発生します。
たとえば、32ビット用にビルドされたDLLを64ビットプロセスで読み込もうとすると BadImageFormatException
が発生しますが、ILコードの生成ミスやコンパイラのバグで不正なILが生成された場合は InvalidProgramException
が発生します。
このように、両者は似ていますが、発生原因と対処方法が異なるため、例外の種類を正しく把握することが重要です。
発生タイミング
InvalidProgramException
は、主に以下のタイミングで発生します。
- JITコンパイル時
C#のコードは中間言語(IL)にコンパイルされ、実行時にJIT(Just-In-Time)コンパイラによってネイティブコードに変換されます。
このJITコンパイルの段階で、ILコードに不正な命令や構造的な問題があると、InvalidProgramException
がスローされます。
- 動的コード生成時
System.Reflection.Emit
や Expression Tree を使って動的にILコードを生成する場合、生成したILが不正だとこの例外が発生します。
たとえば、ローカル変数の型が不整合だったり、例外処理ブロックの構造が間違っている場合です。
- アセンブリの読み込み後、メソッド呼び出し時
アセンブリ自体は読み込めても、特定のメソッドを呼び出した際にそのメソッドのILが不正であると判明し、例外が発生します。
- AOT(Ahead-Of-Time)コンパイル環境での問題
UnityのIL2CPPやXamarinのAOTコンパイルなど、ILを事前にネイティブコードに変換する環境で、ILの不整合が原因で実行時に例外が発生することがあります。
このように、InvalidProgramException
は実行時のコード変換やメソッド呼び出しの直前に発生することが多いです。
発生した場合は、どのメソッドで例外が起きているかを特定し、そのメソッドのILコードやビルド設定を重点的に調査することが重要です。
以下に、InvalidProgramException
が発生する典型的な例を示します。
using System;
class Program
{
static void Main()
{
try
{
// 故意に不正なILを生成することは難しいため、
// ここでは例外が発生する可能性のあるコードを示します。
InvalidMethod();
}
catch (InvalidProgramException ex)
{
Console.WriteLine("InvalidProgramExceptionが発生しました:");
Console.WriteLine(ex.Message);
}
}
static void InvalidMethod()
{
// ここに不正なILが含まれていると仮定します。
// 通常のC#コードでは発生しにくいですが、
// 動的生成やコンパイラバグで発生することがあります。
Console.WriteLine("このメソッドでInvalidProgramExceptionが発生する可能性があります。");
}
}
InvalidProgramExceptionが発生しました:
プログラムが無効です。
この例はあくまでイメージですが、実際には動的IL生成やコンパイラのバグ、ビルド設定の不整合などが原因で発生します。
発生原因を分類する
コンパイル時起因のIL破損
旧バージョンのC#コンパイラバグ
古いC#コンパイラには、ILコードを正しく生成できないバグが存在することがあります。
特にC# 5以前のバージョンでは、特定の構文やジェネリックの組み合わせで不正なILが生成されるケースが報告されています。
こうしたバグにより、ビルドは成功しても実行時にInvalidProgramException
が発生することがあります。
たとえば、非同期メソッドの状態マシン生成や、複雑なラムダ式の展開時にILが破損しやすい傾向があります。
これらはコンパイラの内部処理の問題であり、ソースコード上は特に問題がないように見えることも多いです。
対策としては、最新のC#コンパイラや.NET SDKにアップデートすることが有効です。
Visual Studioのバージョンアップや、<LangVersion>
タグをlatest
に設定することで、バグ修正済みのコンパイラを利用できます。
サードパーティ製コードジェネレータの不具合
コードジェネレータやテンプレートエンジン、ORMツールなどのサードパーティ製品が生成するILコードに問題がある場合もあります。
これらのツールは自動的にコードを生成するため、内部のロジックにバグがあると不正なILが生成され、InvalidProgramException
を引き起こします。
特に、動的にコードを生成してアセンブリに埋め込むタイプのツールは注意が必要です。
生成されたコードの型やメソッドのシグネチャが不整合だったり、例外処理の構造が不正な場合に例外が発生します。
対処法としては、ツールのバージョンアップや、生成コードの手動検査、可能であれば生成コードのILを逆アセンブルして問題箇所を特定することが挙げられます。
実行時のプラットフォーム不一致
x86とx64のミスマッチ
32ビット(x86)と64ビット(x64)のアーキテクチャが混在すると、InvalidProgramException
やBadImageFormatException
が発生しやすくなります。
たとえば、64ビットプロセスで32ビット用にビルドされたDLLを読み込もうとすると問題が起きます。
このミスマッチは、プロジェクトのビルド設定でPlatformTarget
が適切に設定されていない場合に起こります。
Visual Studioの「ビルド」タブでAny CPU
やx64
、x86
の設定を確認し、実行環境に合わせて統一することが重要です。
また、Prefer 32-bit
オプションが有効になっていると、64ビット環境でも32ビットモードで動作しようとするため、意図しない動作を招くことがあります。
AnyCPUでのP/Invoke呼び出し
AnyCPU
設定のアセンブリが、ネイティブコードのDLLをP/Invokeで呼び出す場合、呼び出し先のDLLのビット数と実行プロセスのビット数が一致しないとInvalidProgramException
が発生することがあります。
たとえば、64ビット環境でAnyCPU
ビルドのマネージコードが32ビットのネイティブDLLを呼び出すと、ILコード自体は正しくても実行時に例外が発生します。
この問題を回避するには、P/Invokeで呼び出すDLLのビット数に合わせて、マネージコードのビルド設定を明示的にx86
またはx64
に固定することが推奨されます。
動的コード生成の問題
Reflection.EmitでのILミス
System.Reflection.Emit
を使って動的にILコードを生成する際、IL命令の順序や型の整合性が正しくないとInvalidProgramException
が発生します。
たとえば、ローカル変数の型を間違えたり、例外処理ブロックの開始・終了が不正確だとJITコンパイラがエラーを検出します。
以下は、Reflection.Emit
でローカル変数の型を誤って定義した場合の例です。
using System;
using System.Reflection;
using System.Reflection.Emit;
class Program
{
static void Main()
{
try
{
DynamicMethod method = new DynamicMethod("InvalidMethod", typeof(void), Type.EmptyTypes);
ILGenerator il = method.GetILGenerator();
// ローカル変数をint型で宣言
LocalBuilder local = il.DeclareLocal(typeof(int));
// しかし、string型の値を代入しようとしている(型不整合)
il.Emit(OpCodes.Ldstr, "不正な型代入");
il.Emit(OpCodes.Stloc, local);
il.Emit(OpCodes.Ret);
var action = (Action)method.CreateDelegate(typeof(Action));
action();
}
catch (InvalidProgramException ex)
{
Console.WriteLine("InvalidProgramExceptionが発生しました:");
Console.WriteLine(ex.Message);
}
}
}
InvalidProgramExceptionが発生しました:
プログラムが無効です。
この例では、ローカル変数の型と代入しようとする値の型が一致していないため、JITコンパイラがILの不整合を検出し例外をスローしています。
Expression TreeのJIT最適化バグ
Expression Treeを使って動的にコードを生成し、コンパイルした際にJITコンパイラの最適化処理が原因でInvalidProgramException
が発生することがあります。
特に複雑な式や再帰的な式の展開で発生しやすいです。
この問題は.NETの特定バージョンで報告されており、最新のランタイムにアップデートすることで解消されることが多いです。
また、Expression Treeの構造を単純化することも有効です。
DLL・NuGetパッケージの破損
ダウンロード途中での壊れ
NuGetパッケージやDLLファイルがダウンロード途中で破損した場合、アセンブリのメタデータやILコードが不完全になり、InvalidProgramException
が発生することがあります。
特にネットワーク障害やキャッシュの不整合が原因です。
この場合は、パッケージの再インストールやキャッシュのクリアを行うことで解決します。
Visual Studioの「パッケージマネージャーコンソール」でdotnet nuget locals --clear all
コマンドを実行し、ローカルキャッシュをクリアすることが推奨されます。
不正な署名やバージョン不整合
署名付きのアセンブリで署名が破損していたり、依存関係のバージョンが不整合な場合もInvalidProgramException
が発生します。
たとえば、異なるバージョンの同一DLLが混在していると、ILコードの整合性が崩れることがあります。
依存関係のバージョンを統一し、署名の検証を行うことが重要です。
Assembly Binding Log Viewer (Fuslogvw.exe)
を使って依存関係の読み込み状況を確認するとよいでしょう。
AOT・Unity・Xamarin特有の要因
IL2CPP変換時の欠落
UnityのIL2CPPはILコードをC++に変換してネイティブコードにコンパイルしますが、この変換過程で特定のメソッドや型が正しく変換されないことがあります。
結果として、実行時にInvalidProgramException
が発生することがあります。
特にリフレクションで参照されるメソッドが変換対象から除外されている場合や、ジェネリック型の特殊化が不完全な場合に起こりやすいです。
対策としては、link.xml
ファイルで必要な型やメソッドを明示的に保持する設定を行うことが推奨されます。
リンク時ストリッピング
XamarinやUnityのビルドプロセスで不要なコードを削除する「ストリッピング」機能が有効になっていると、実際には使用しているメソッドや型が誤って削除され、ILコードの整合性が崩れることがあります。
これにより、実行時にInvalidProgramException
が発生します。
ストリッピングの設定を見直し、必要なコードを除外リストに追加することで回避可能です。
オブファスケーションの副作用
コントロールフロー変換の失敗
オブファスケーションツールはコードの解析を困難にするために、コントロールフローを複雑化する変換を行います。
しかし、この変換が不完全だったり、ILコードの構造を破壊するとInvalidProgramException
が発生します。
特に例外処理ブロックやジャンプ命令の不整合が原因となることが多いです。
オブファスケーションの設定でコントロールフロー変換を無効化するか、例外処理を含むメソッドを除外することが対策になります。
無効なメタデータ生成
オブファスケーション時にメタデータの書き換えが誤って行われると、型情報やメソッドシグネチャが不正になり、ILコードの整合性が崩れます。
これによりInvalidProgramException
が発生します。
メタデータの保護設定を適切に行い、信頼できるオブファスケーションツールを使用することが重要です。
また、オブファスケーション後にIL検証ツールを使って問題がないかチェックすることも推奨されます。
代表的なスタックトレース例
コンパイラ生成メソッドでの発生
C#のコンパイラは、非同期メソッドやイテレータ、ラムダ式などの特殊な構文を使うと、内部的に状態マシンや匿名メソッドを生成します。
これらのコンパイラ生成メソッドでILコードに問題があると、InvalidProgramException
が発生しやすくなります。
以下は、非同期メソッドの状態マシン生成に起因する例外のスタックトレース例です。
System.InvalidProgramException: プログラムが無効です。
場所 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1[[System.Int32, System.Private.CoreLib]].Start[[MyNamespace.MyClass+<MyAsyncMethod>d__0, MyAssembly]](!!0&)
場所 MyNamespace.MyClass.<MyAsyncMethod>d__0.MoveNext()
場所 System.Runtime.CompilerServices.AsyncTaskMethodBuilder`1`1.SetResult(!0)
場所 System.Threading.Tasks.Task`1.TrySetResult(!0)
場所 System.Threading.Tasks.TaskCompletionSource`1.SetResult(!0)
この例では、<MyAsyncMethod>d__0
という名前のコンパイラ生成クラスのMoveNext
メソッドで例外が発生しています。
これは非同期メソッドの状態遷移を管理するメソッドであり、ILコードの破損やコンパイラのバグが原因でInvalidProgramException
がスローされることがあります。
このような場合は、C#コンパイラのバージョンアップや、非同期メソッドの書き方を見直すことが有効です。
また、プロジェクトのビルド設定やターゲットフレームワークの整合性も確認してください。
自作ライブラリ内部での発生
自作ライブラリの内部でInvalidProgramException
が発生するケースも多く見られます。
特に、動的にコードを生成したり、複雑なジェネリック型やunsafeコードを使っている場合に起こりやすいです。
以下は、自作ライブラリのメソッド呼び出し時に発生した例のスタックトレースです。
System.InvalidProgramException: プログラムが無効です。
場所 MyLibrary.InternalClass.ProcessData(System.String data)
場所 MyLibrary.PublicClass.Execute()
場所 MyApp.Program.Main(System.String[] args)
この例では、MyLibrary.InternalClass.ProcessData
メソッド内で例外が発生しています。
ILコードの不整合や、ビルド時の設定ミス、依存関係の不整合が原因となることが多いです。
対策としては、問題のメソッドを特定し、IL検証ツール(ILDASMやILSpy)で逆アセンブルしてILコードの整合性を確認します。
また、ビルド設定の見直しや、依存DLLのバージョン管理も重要です。
サードパーティDLL呼び出し時の発生
サードパーティ製のDLLを呼び出す際にInvalidProgramException
が発生することもあります。
特に、DLLのバージョン違いやプラットフォーム不一致、破損したDLLを読み込んだ場合に起こりやすいです。
以下は、サードパーティDLLのメソッド呼び出しで発生した例のスタックトレースです。
System.InvalidProgramException: プログラムが無効です。
場所 ThirdPartyLibrary.SomeClass.SomeMethod()
場所 MyApp.Program.Main(System.String[] args)
この例では、ThirdPartyLibrary.SomeClass.SomeMethod
の呼び出し時に例外が発生しています。
DLLのビルド設定や依存関係の不整合、署名の問題などが原因となることが多いです。
対処法としては、DLLの再取得やバージョンアップ、プラットフォーム設定の統一を行います。
また、DLLの整合性を検証するためにIL検証ツールを使うことも有効です。
場合によっては、サードパーティのサポートに問い合わせることも検討してください。
調査とデバッグのアプローチ
再現手順の最小化
問題コードの切り出し
InvalidProgramException
の調査では、まず問題が発生するコードを最小限に切り出すことが重要です。
大規模なプロジェクトや複雑な処理の中で例外が発生すると、原因特定が難しくなります。
そこで、問題の発生するメソッドやクラスを単独のコンソールアプリやテストプロジェクトに切り出し、再現性のある最小限のコードに絞り込みます。
例えば、動的コード生成を行う部分や、非同期メソッド、ジェネリックを多用する箇所を中心に切り出し、例外が発生するかどうかを確認します。
これにより、問題の範囲を限定し、原因の特定や修正がしやすくなります。
データ・構成の固定
例外が特定の入力データや環境設定に依存している場合は、再現性を高めるためにデータや構成を固定します。
たとえば、外部ファイルやデータベースの状態、環境変数、設定ファイルの内容を一定に保ち、同じ条件下で何度もテストを行います。
これにより、偶発的な環境依存の問題を排除し、原因の切り分けが容易になります。
特に複数の依存関係が絡む場合は、依存DLLのバージョンや配置も固定することが望ましいです。
ILを解析するツール
ILDASMでのメタデータ確認
ILDASM
(IL Disassembler)は、Microsoftが提供するILコードの逆アセンブルツールです。
アセンブリのメタデータやIL命令を視覚的に確認でき、ILの構造やメソッドの中身を詳細に調査できます。
ILDASM
を使うと、メソッドのILコードが正しく生成されているか、例外処理ブロックの構造が整っているかなどを確認できます。
特にInvalidProgramException
の原因がILの破損や不整合にある場合、問題箇所の特定に役立ちます。
ILSpy / dotPeekでの逆アセンブル
ILSpy
やdotPeek
は、.NETアセンブリを逆アセンブルしてC#のソースコードに近い形で表示できるツールです。
これらを使うと、ILコードだけでなく、元のソースコードの構造やロジックを把握しやすくなります。
問題のあるメソッドやクラスを特定し、どのようなコードが生成されているかを確認することで、コンパイル時の問題やコード生成の誤りを見つけやすくなります。
ILVerifyでの静的検査
ILVerify
は、ILコードの静的検証ツールで、ILの整合性やメタデータの正当性をチェックします。
Visual Studioの拡張機能やコマンドラインツールとして利用可能です。
ILVerify
を使うと、ILの不正な命令や構造的な問題を事前に検出でき、InvalidProgramException
の原因を早期に発見できます。
CI環境に組み込むことで、ビルド時に問題を検出することも可能です。
JITデバッグオプションの活用
COMPlus_ReadyToRun=0の設定
.NET
のJITコンパイルには、事前コンパイル済みのコード(ReadyToRun)が利用されることがありますが、これが原因で問題が隠れる場合があります。
環境変数COMPlus_ReadyToRun=0
を設定すると、ReadyToRunコードを無効化し、純粋なJITコンパイルを強制できます。
これにより、JITコンパイル時の問題が明確になり、InvalidProgramException
の再現性が向上します。
コマンドプロンプトやPowerShellで以下のように設定してからアプリを実行します。
set COMPlus_ReadyToRun=0
dotnet run
COMPlus_JitStressでの再現性向上
COMPlus_JitStress
はJITコンパイラのストレステスト用環境変数で、JITの最適化やコード生成の挙動を変化させます。
これを設定すると、通常は発生しにくいILの問題が顕在化しやすくなります。
たとえば、COMPlus_JitStress=1
やCOMPlus_JitStress=2
などの値を設定して実行することで、InvalidProgramException
の再現性を高め、原因調査に役立てられます。
プラットフォーム設定の確認
Visual Studioのビルド構成
Visual Studioのビルド設定で、Platform Target
やPrefer 32-bit
の設定が不適切だと、実行時にInvalidProgramException
が発生することがあります。
特に、x86とx64の混在やAnyCPU設定の誤用が原因です。
プロジェクトのプロパティから「ビルド」タブを開き、Platform target
を実行環境に合わせて明示的に設定します。
また、Prefer 32-bit
のチェックを外すか入れるかも環境に応じて調整してください。
PublishSingleFile時の注意点
.NET Coreや.NET 5以降のPublishSingleFile
機能を使うと、すべての依存ファイルを単一の実行ファイルにまとめられますが、この機能が原因でILの読み込みやJITコンパイルに問題が生じることがあります。
特にネイティブライブラリのロードや動的コード生成を行う場合は、PublishSingleFile
の設定を見直し、必要に応じてIncludeAllContentForSelfExtract
やSelfContained
のオプションを調整してください。
NuGet依存関係の点検
バージョン競合の検出
複数のNuGetパッケージが異なるバージョンの同一依存ライブラリを参照していると、実行時に不整合が生じてInvalidProgramException
が発生することがあります。
これを「依存関係の競合」と呼びます。
Visual Studioの「依存関係」ツリーやdotnet list package --include-transitive
コマンドで依存関係を確認し、バージョンを統一することが重要です。
bindingRedirect
の設定も適切に行いましょう。
参照外れDLLの削除
プロジェクトに不要なDLLや古いバージョンのDLLが混在していると、実行時に誤ったDLLが読み込まれ、ILの不整合が起きることがあります。
これによりInvalidProgramException
が発生します。
ビルド出力フォルダをクリーンし、bin
やobj
フォルダを削除してから再ビルドすることが推奨されます。
また、NuGetパッケージの再インストールも効果的です。
ソースコードレベルのチェック
unsafeコードとポインタ演算
unsafe
キーワードを使ったポインタ操作やアンマネージドコードとの連携は、ILコードの整合性を崩しやすい箇所です。
ポインタの型不一致や不正なメモリアクセスがあると、JITコンパイル時にInvalidProgramException
が発生することがあります。
コードレビューや静的解析ツールを使い、unsafe
コードの安全性と正当性を確認してください。
ジェネリック制約の誤用
ジェネリック型やメソッドの制約を誤って設定すると、ILコードの型安全性が損なわれ、実行時に例外が発生することがあります。
特に複雑なジェネリックのネストや型パラメータの不整合が原因です。
ジェネリックの制約を見直し、必要に応じて単純化することが効果的です。
CI/CD環境での再現テスト
コンテナビルドでの差分検証
CI/CDパイプラインでコンテナを使ったビルド環境を構築し、ローカル環境との違いを検証します。
環境差異によるInvalidProgramException
の発生を防ぐため、ビルド環境をコード化し、再現性を確保します。
Dockerfileやビルドスクリプトを用いて、依存関係やSDKバージョンを固定し、問題の切り分けに役立てます。
マルチアーキテクチャテスト
x86、x64、ARMなど複数のプラットフォームでビルド・テストを自動化し、プラットフォーム依存の問題を早期に検出します。
これにより、プラットフォーム不一致によるInvalidProgramException
の発生を未然に防げます。
GitHub ActionsやAzure PipelinesなどのCIツールでマルチプラットフォームビルドを設定し、継続的に検証することが推奨されます。
主な対処法
ビルド設定の統一
Prefer32Bitフラグの扱い
Visual Studioのプロジェクト設定にあるPrefer 32-bit
フラグは、AnyCPUターゲットでビルドした場合に64ビット環境でも32ビットモードで実行するかどうかを制御します。
この設定が原因で、64ビット環境で32ビットDLLを読み込もうとしてInvalidProgramException
が発生することがあります。
対処法としては、64ビット環境で動作させる場合はPrefer 32-bit
のチェックを外し、64ビットネイティブで動作させることが推奨されます。
逆に32ビット環境で動作させる場合はチェックを入れたままにします。
環境に合わせて適切に設定し、ビルド構成を統一することが重要です。
PlatformTargetの選定指針
PlatformTarget
は、ビルド時に生成されるアセンブリの対象プラットフォームを指定します。
AnyCPU
、x86
、x64
などがありますが、実行環境や依存DLLのビット数に合わせて選定しなければなりません。
- 依存DLLが32ビットのみの場合は
x86
を選択 - 64ビット環境で動作させる場合は
x64
を選択 - 汎用的に動作させたい場合は
AnyCPU
を選択し、Prefer 32-bit
の設定を調整
ビルド構成を複数用意する場合は、CI/CDでビルドごとに適切な設定を使い分けることが望ましいです。
破損DLLの置き換え
クリーンリストア
破損したDLLや不整合な依存関係が原因でInvalidProgramException
が発生する場合、プロジェクトのクリーンリストアを行います。
Visual Studioの「ビルド」メニューから「クリーン」を実行し、bin
やobj
フォルダを手動で削除してから再ビルドします。
また、NuGetパッケージの再インストールも効果的です。
dotnet restore
コマンドやVisual Studioのパッケージマネージャーでパッケージを再取得し、破損したDLLを置き換えます。
キャッシュクリア方法
NuGetのローカルキャッシュが破損している場合も問題が発生します。
以下のコマンドでキャッシュをクリアし、再度パッケージを取得してください。
dotnet nuget locals all --clear
Visual Studioの「ツール」→「オプション」→「NuGetパッケージマネージャー」からもキャッシュのクリアが可能です。
キャッシュクリア後は必ずプロジェクトを再ビルドしてください。
コンパイラおよびランタイムの更新
.NET SDKのバージョン固定
コンパイラやランタイムのバージョン差異が原因でILコードの生成やJITコンパイルに問題が生じることがあります。
特に複数の開発者やCI環境でバージョンが異なる場合は注意が必要です。
global.json
ファイルをプロジェクトルートに配置し、使用する.NET SDKのバージョンを固定することで、ビルド環境の一貫性を保てます。
{
"sdk": {
"version": "7.0.100"
}
}
Visual Studioの最新化
Visual Studioのバージョンが古いと、コンパイラやビルドツールにバグが残っている可能性があります。
最新の安定版にアップデートし、バグ修正や機能改善を取り込むことが推奨されます。
また、拡張機能やツールチェーンも最新に保つことで、ビルド時の問題を減らせます。
動的IL生成の修正
LocalBuilderの型整合性
System.Reflection.Emit
を使った動的IL生成では、LocalBuilder
で宣言したローカル変数の型と、実際に代入する値の型が一致している必要があります。
型不整合があるとInvalidProgramException
が発生します。
以下のサンプルは正しい型整合性の例です。
using System;
using System.Reflection.Emit;
class Program
{
static void Main()
{
DynamicMethod method = new DynamicMethod("Hello", null, null);
ILGenerator il = method.GetILGenerator();
// int型のローカル変数を宣言
LocalBuilder local = il.DeclareLocal(typeof(int));
// int型の値を代入
il.Emit(OpCodes.Ldc_I4, 42);
il.Emit(OpCodes.Stloc, local);
il.Emit(OpCodes.Ret);
var action = (Action)method.CreateDelegate(typeof(Action));
action();
Console.WriteLine("動的IL生成が成功しました。");
}
}
動的IL生成が成功しました。
型の不一致がないか、ILコードを丁寧に確認してください。
例外処理ブロックの正当性
動的IL生成で例外処理(try-catch-finally)を実装する場合、例外ブロックの開始・終了位置やネスト構造が正確でなければなりません。
不正な例外処理ブロックはJITコンパイラでエラーとなり、InvalidProgramException
を引き起こします。
ILGeneratorのBeginExceptionBlock
、BeginCatchBlock
、EndExceptionBlock
の呼び出し順序を厳密に守り、例外処理の構造を正しく構築してください。
オブファスケーション設定の見直し
フロー制御変換の無効化
オブファスケーションツールの中には、コントロールフローを複雑化する「フロー制御変換」機能がありますが、これが原因でILコードが破損しInvalidProgramException
が発生することがあります。
問題が疑われる場合は、オブファスケーションの設定でフロー制御変換を無効化し、例外が解消されるか確認してください。
属性保持ルールの追加
オブファスケーション時に特定の属性やメタデータが削除されると、ILコードの整合性が崩れることがあります。
重要な属性(例:MethodImplAttribute
やCompilerGeneratedAttribute
)は保持するように設定を追加してください。
これにより、JITコンパイル時の不整合を防げます。
AOT / モバイル環境での調整
IL2CPPのブラックリスト登録
UnityのIL2CPP変換時に、特定のメソッドや型が変換対象から除外されていると、実行時にInvalidProgramException
が発生します。
link.xml
ファイルで必要な型やメソッドをブラックリストに登録し、変換から除外することで回避可能です。
<linker>
<assembly fullname="AssemblyName">
<type fullname="Namespace.TypeName" preserve="all" />
</assembly>
</linker>
Linker設定での除外
XamarinやUnityのLinker(コードストリッピング)機能が過剰に働くと、必要なコードが削除されてILの整合性が崩れます。
Linkerの設定で除外リストを作成し、重要なコードを保持してください。
Visual Studioのプロジェクト設定やlinker.xml
で除外設定を行い、ストリッピングの影響を最小限に抑えます。
再リリース前の検証スクリプト
ILVerifyのCI組み込み
ILVerify
をCIパイプラインに組み込み、ビルド時にILコードの整合性を自動検査します。
これにより、InvalidProgramException
の原因となるIL破損を早期に検出し、リリース前に修正できます。
GitHub ActionsやAzure PipelinesでILVerify
を実行するスクリプトを作成し、継続的に品質を担保しましょう。
P/Invokeシグネチャテスト
P/InvokeでネイティブDLLを呼び出す場合、シグネチャの不整合がInvalidProgramException
を引き起こすことがあります。
テストコードを用意し、すべてのP/Invoke呼び出しが正しく動作するか検証してください。
特に引数の型や呼び出し規約、文字列のマシュリング設定を厳密にチェックし、問題があれば修正します。
予防策とベストプラクティス
プラットフォーム別ビルドの自動化
プラットフォームごとに異なるビルド設定を手動で管理すると、設定ミスやビット数の不整合が発生しやすくなります。
これを防ぐために、ビルドプロセスを自動化し、プラットフォーム別のビルドを確実に行うことが重要です。
たとえば、CI/CDパイプラインでx86
、x64
、AnyCPU
など複数のビルド構成を用意し、それぞれの環境に適したビルドを自動的に生成・テストします。
Azure PipelinesやGitHub ActionsなどのCIツールを活用し、ビルドスクリプトにdotnet build
コマンドの-r
(ランタイム識別子)や-p:Platform
オプションを組み込むと効果的です。
こうした自動化により、プラットフォーム間の不整合を早期に検出でき、InvalidProgramException
の発生リスクを減らせます。
依存ライブラリのチェックリスト
依存ライブラリのバージョンや整合性が乱れると、ILコードの不整合や実行時エラーの原因になります。
依存関係を管理するために、チェックリストを作成し、以下のポイントを定期的に確認します。
- すべての依存ライブラリのバージョンが統一されているか
- 署名付きアセンブリの署名が有効か
- 不要な依存ライブラリが含まれていないか
- NuGetパッケージのキャッシュが破損していないか
- 依存関係のトランジティブなバージョン競合がないか
このチェックリストをCIの一部として組み込み、依存関係の問題を早期に発見・修正することが推奨されます。
動的コード生成のユニットテスト
動的にILコードを生成する処理は、InvalidProgramException
の原因になりやすいため、ユニットテストを充実させることが重要です。
生成したコードが正しく動作するか、型の整合性や例外処理の構造が正しいかをテストで検証します。
たとえば、Reflection.Emit
やExpression Treeを使う部分に対しては、生成コードの実行テストやIL検証ツールを組み合わせてテストケースを作成します。
異常系のテストも含め、例外が発生しないことを確認することで、問題の早期発見につながります。
CIでの定期的な静的IL検査
CI環境にILVerify
などの静的IL検査ツールを組み込み、ビルドごとにILコードの整合性をチェックします。
これにより、ILの破損や不正な命令が含まれていないかを自動的に検出でき、InvalidProgramException
の原因を未然に防げます。
また、IL検査の結果をレポートとして出力し、開発チームで共有することで品質管理を強化できます。
CIでの自動検査は、リリース前の品質保証に欠かせないプロセスです。
ユーザー環境別クラッシュログ収集
実際のユーザー環境で発生するInvalidProgramException
は、環境依存の問題であることが多いため、クラッシュログの収集と分析が重要です。
クラッシュレポートツール(例:App Center、Sentry、Raygunなど)を導入し、例外発生時のスタックトレースや環境情報を収集します。
収集したログをもとに、特定のOSバージョンやCPUアーキテクチャ、インストールされている依存DLLのバージョンなどを分析し、問題の再現条件を特定します。
これにより、環境依存のInvalidProgramException
を効率的に解決できます。
まとめ
この記事では、C#で発生するInvalidProgramException
の原因や特徴、代表的なスタックトレース例を紹介しました。
発生原因はコンパイル時のIL破損やプラットフォーム不一致、動的コード生成の問題など多岐にわたります。
調査にはIL解析ツールやJITデバッグオプションが有効で、ビルド設定の統一や破損DLLの置き換え、コンパイラの更新などが主な対処法です。
さらに、予防策としてプラットフォーム別ビルドの自動化や依存関係の管理、CIでの静的IL検査を推奨します。
これらを実践することで、InvalidProgramException
の発生を抑え、安定したアプリケーション開発が可能になります。