エラー メッセージは、ESP レジスタ (スタック ポインタ) が適切に「維持」されていないことを示しています。持つべき価値がありません。
C や C++ などのアンマネージ言語で関数呼び出しを行うと、関数への引数がスタックにプッシュされ、スタック ポインターが増加します。関数呼び出しが戻ると、引数がポップバックされ、スタック ポインターが減少します。
スタック ポインターは、常に関数呼び出し前と同じ値に復元する必要があります。
呼び出し規約
呼び出し規則は、スタックを維持する方法と、呼び出し元または呼び出し先がスタックから引数をポップする責任があるかどうかを正確に指定します。
たとえば、stdcall 呼び出し規約では、呼び出しeeは、関数が戻る前にスタック ポインターを復元する責任があります。cdecl呼び出し規約では、呼び出し元が責任を負います。
呼び出し規約を混在させることが悪いことであることは明らかです。呼び出し元が stdcall を使用している場合、呼び出し先がスタックを維持することを期待しています。呼び出し先が cdecl を使用している場合、呼び出し元がスタックを維持することを期待しています。最終結果: 誰もスタックを維持していません! またはその反対の例: 誰もがスタックを維持しています。
参考までに、この StackOverflow questionを見てください。
Raymond Chen は、このテーマに関する優れたブログ投稿を行っています。
どの呼び出し規約を使用する必要がありますか?
それはこの回答の範囲を超えていますが、C# から C への相互運用を行っている場合は、どの呼び出し規約が適用されているかを知ることが重要です。
Visual Studio では、C/C++ プロジェクトの既定の呼び出し規約は cdecl です。
.Net では、DllImport を使用した相互運用呼び出しの既定の呼び出し規則は stdcall です。これは代表者にも当てはまります。(ほとんどのネイティブ Windows 関数は stdcall を使用します。)
次の (正しくない) 相互運用呼び出しを検討してください。
[DllImport("MyDll", EntryPoint = "MyDll_Init"]
public static extern void Init();
それは.Netのデフォルトであるため、stdcall呼び出し規約を使用しています。MyDLL プロジェクトの Visual Studio プロジェクト設定を変更していない場合、これが機能しないことがすぐにわかります。C/C++ DLL プロジェクトのデフォルトは cdecl です。
正しい相互運用呼び出しは次のようになります。
[DllImport("MyDll", EntryPoint = "MyDll_Init", CallingConvention = CallingConvention.Cdecl)]
public static extern void Init();
明示的な CallingConvention 属性に注意してください。C# 相互運用ラッパーは、cdecl 呼び出しを生成することを認識します。
他に何が問題になる可能性がありますか?
呼び出し規約が正しいと確信している場合でも、実行時チェックの失敗 #0 が発生する可能性があります。
構造体のマーシャリング
関数の引数は、関数呼び出しの開始時にスタックにプッシュされ、最後に再びポップされることを思い出してください。スタックが正しく維持されるようにするには、プッシュとポップの間で引数のサイズが一致している必要があります。
ネイティブ コードでは、コンパイラがこれを処理します。考える必要はありません。C と C# の間の相互運用に関しては、噛まれるかもしれません。
C# に stdcall デリゲートがある場合は、次のようになります。
public delegate void SampleTimeChangedCallback(SampleTime sampleTime);
これは、次のような C 関数ポインターに対応します。
typedef void(__stdcall *SampleTimeChangedCallback)(SampleTime sampleTime);
すべてがうまくいくはずです。両側で同じ呼び出し規約を使用しています (C# 相互運用機能は既定で stdcall を使用し、ネイティブ コードで明示的に __stdcall を設定しています)。
しかし、これらのパラメーターを見てください: SampleTime 構造体です。どちらも同じ名前ですが、一方はネイティブ構造体、もう一方は C# 構造体です。
ネイティブ構造体は次のようになります。
struct SampleTime
{
__int64 displayTime;
__int64 playbackTime;
}
C# 構造体は次のようになります。
[StructLayout(LayoutKind.Explicit, Size = 32)]
public struct SampleTime
{
[FieldOffset(0)]
private long displayTime;
[FieldOffset(8)]
private long playbackTime;
}
C# 構造体の Size 属性を見てください - それは間違っています! 2 つの 8 バイトの long は、16 バイトのサイズを意味します。誰かがいくつかのフィールドを削除して、Size 属性を更新できなかった可能性があります。
ここで、ネイティブ コードが stdcall を使用して SampleTimeChangedCallback 関数を呼び出すと、問題が発生します。
stdcall では、呼び出し先(つまり、呼び出される関数) がスタックを復元する責任があることを思い出してください。
したがって、呼び出し元はパラメーターをスタックにプッシュします。この例では、それがネイティブ コードで発生しています。パラメータのサイズはコンパイラによって認識されるため、スタック ポインタがインクリメントされる値は正しいことが保証されます。
次に関数が実行されます - 実際にはこれは ac# デリゲートであることを思い出してください。
stdcall を使用しているため、呼び出し先 (c# デリゲート) がスタックの復元を担当します。しかし、C# の世界では、実際には 16 バイトしかない SampleTime 構造体のサイズが 32 バイトであるとコンパイラに嘘をつきました。
One Definition Ruleに違反しました。
C# コンパイラには、私たちの言うことを信じる以外に選択肢がないため、スタック ポインタを 32 バイト単位で「復元」します。
コールサイト (ネイティブ ランド) に戻ると、スタック ポインターが適切に復元されておらず、すべてのベットがオフになっています。
運が良ければ、実行時チェック #0 に遭遇するでしょう。運が悪いと、プログラムがすぐにクラッシュしない可能性があります。1 つ確かなことは、プログラムが、思っていたコードを実行していないということです。