web-dev-qa-db-ja.com

「標準」のP / InvokeコンベンションでCdecl呼び出しがしばしば一致しないのはなぜですか?

私は、C++の機能がC#からP/Invokedであるかなり大きなコードベースに取り組んでいます。

コードベースには次のような多くの呼び出しがあります...

C++:

extern "C" int __stdcall InvokedFunction(int);

対応するC#の場合:

[DllImport("CPlusPlus.dll", ExactSpelling = true, SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
    private static extern int InvokedFunction(IntPtr intArg);

この明らかなミスマッチが存在する理由を推論するために、私はネットを精査しました(私はできる限り)。たとえば、C#内にCdeclがあり、C++内に__stdcallがあるのはなぜですか?どうやら、これによりスタックが2回クリアされますが、どちらの場合も変数は同じ逆順でスタックにプッシュされ、エラーが表示されないようにします。ただし、デバッグ中にトレースを試行しますか?

MSDNから: http://msdn.Microsoft.com/en-us/library/2x8kf7zx%28v=vs.100%29.aspx

// explicit DLLImport needed here to use P/Invoke marshalling
[DllImport("msvcrt.dll", EntryPoint = "printf", CallingConvention = CallingConvention::Cdecl,  CharSet = CharSet::Ansi)]

// Implicit DLLImport specifying calling convention
extern "C" int __stdcall MessageBeep(int);

もう一度、両方がありますextern "C"はC++コードで、CallingConvention.CdeclはC#です。なぜそうではないCallingConvention.Stdcall?または、さらに、なぜあるのか__stdcall C++では?

前もって感謝します!

53
Kadaj Nakamura

これはSOの質問で繰り返し出てきます。これを(長い)参照回答に変えようとします。32ビットコードには互換性のない呼び出し規約の長い歴史があります。 64ビットコードには呼び出し規約が1つしかないため、別の呼び出し規約を追加しようとすると、小さな島に送られます。南大西洋。

Wikipediaの記事 にあるものを超えて、それらの歴史と関連性に注釈を付けようとします。出発点は、関数呼び出しを行う方法で行うべき選択は、引数を渡す順序、引数を保存する場所、および呼び出し後にクリーンアップする方法であるということです。

  • ___stdcall_は、16ビットWindowsおよびOS/2で使用されていた古い16ビットPascal呼び出し規約により、Windowsプログラミングに取り入れられました。これは、すべてのWindows API関数とCOMで使用される規則です。ほとんどのpinvokeはOS呼び出しを行うことを目的としているため、[DllImport]属性で明示的に指定しない場合、Stdcallがデフォルトです。存在する唯一の理由は、呼び出し先がクリーンアップすることを指定することです。これは、よりコンパクトなコードを生成します。これは、640キロバイトのRAMでGUIオペレーティングシステムを圧縮する必要があった時代に非常に重要です。最大の欠点は、dangerousであることです。呼び出し元が関数の引数であると想定するものと、呼び出し先が実装したものがスタックの不均衡を引き起こすものとの間の不一致。これにより、クラッシュの診断が非常に難しくなります。

  • ___cdecl_は、C言語で記述されたコードの標準呼び出し規約です。存在する主な理由は、可変数の引数を使用した関数呼び出しをサポートすることです。 printf()やscanf()などの関数を持つCコードで一般的。副作用として、実際に渡された引数の数を知っているのは呼び出し側であるため、クリーンアップするのは呼び出し側です。 [DllImport]宣言のCallingConvention = CallingConvention.Cdeclを忘れることは、veryの一般的なバグです。

  • ___fastcall_は、相互に互換性のない選択肢を持つかなり不十分に定義された呼び出し規約です。 Borlandコンパイラは、崩壊するまでコンパイラテクノロジーに大きな影響を与えていた会社で一般的でした。また、C#名声のAnders Hejlsbergを含む多くのマイクロソフト従業員の元雇用者。スタックの代わりにCPUレジスタを介してsomeを渡すことで、引数の受け渡しを安くすることが発明されました。標準化が不十分なため、マネージコードではサポートされていません。

  • ___thiscall_は、C++コード用に考案された呼び出し規約です。 __cdeclと非常に似ていますが、クラスオブジェクトの隠しthisポインターをクラスのインスタンスメソッドに渡す方法も指定します。 Cを超えたC++の特別な詳細。実装は簡単に見えますが、.NET pinvoke marshallerはnotをサポートします。 C++コードをピンボークできない主な理由。複雑さは呼び出し規約ではなく、thisポインターの適切な値です。 C++の多重継承のサポートにより、これは非常に複雑になる可能性があります。何を渡す必要があるかを正確に把握できるのは、C++コンパイラだけです。そして、C++クラスのコードを生成したまったく同じC++コンパイラーだけが、異なるコンパイラーがMIの実装方法と最適化方法について異なる選択をしました。

  • ___clrcall_は、マネージコードの呼び出し規則です。それは、他のもののブレンドです、this__thiscallのようなポインターの受け渡し、__ fastcallのような最適化された引数の受け渡し、__ cdeclのような引数の順序、__ stdcallのような呼び出し元のクリーンアップ。マネージコードの大きな利点は、ジッタに組み込まれたverifierです。これにより、呼び出し元と呼び出し先の間に非互換性がないことを確認できます。したがって、設計者がこれらのすべての規則の利点を活用できるようになりますが、手間がかかりません。コードを安全にするためのオーバーヘッドにもかかわらず、マネージコードがネイティブコードとの競争力を維持する方法の例。

あなたは_extern "C"_に言及しますが、その重要性を理解することは相互運用性を生き残るためにも重要です。言語コンパイラは、エクスポートされた関数の名前に余分な文字を付けて装飾することがよくあります。 「名前マングリング」とも呼ばれます。それはトラブルを引き起こすことを決して止めないかなりくだらないトリックです。また、[DllImport]属性のCharSet、EntryPoint、およびExactSpellingプロパティの適切な値を決定するには、それを理解する必要があります。多くの規則があります:

  • Windows APIデコレーション。 Windowsは元々、文字列に8ビットエンコーディングを使用する非Unicodeオペレーティングシステムでした。 Windows NTは、その中心でUnicodeになった最初のものでした。それはかなり大きな互換性の問題を引き起こし、古いコードは8ビットでエンコードされた文字列をutf-16でエンコードされたUnicode文字列を期待するwinapi関数に渡すため、新しいオペレーティングシステムで実行できませんでした。彼らはこれをすべてのwinapi関数のtwoバージョンを書くことで解決しました。 1つは8ビット文字列を受け取り、もう1つはUnicode文字列を受け取ります。レガシーバージョンの名前の末尾に文字A(A = Ansi)と新しいバージョンの末尾にW(W =ワイド)を接着することで、2つを区別します。関数が文字列を受け取らない場合、何も追加されません。 pinvoke marshallerは、ユーザーの助けなしにこれを自動的に処理します。3つのバージョンすべてを見つけようとします。ただし、常にCharSet.Auto(またはUnicode)を指定する必要があります。文字列をAnsiからUnicodeに変換するレガシー関数のオーバーヘッドは不要であり、損失が大きくなります。

  • __stdcall関数の標準装飾は_foo @ 4です。先頭のアンダースコアと、引数の合計サイズを示す@n後置記号。この接尾辞は、呼び出し元と呼び出し先が引数の数について同意しない場合に、厄介なスタックの不均衡の問題を解決するために設計されました。エラーメッセージは大きくありませんが、うまく機能します。ピンボークマーシャラーは、エントリポイントを見つけることができないことを通知します。注目すべきは、Windowsが__stdcallを使用している間、notがこの装飾を使用しないことです。これは意図的なもので、プログラマーにGetProcAddress()引数を正しく取得するためのショットを与えました。 pinvoke marshallerはこれを自動的に処理し、最初に@n接尾辞を持つエントリポイントを見つけようとし、次になしのエントリポイントを試みます。

  • __cdecl関数の標準の装飾は_fooです。単一の先行アンダースコア。 pinvoke marshallerはこれを自動的にソートします。悲しいことに、__ stdcallのオプションの@n接尾辞では、CallingConventionプロパティが間違っていること、大きな損失であることを知らせることができません。

  • C++コンパイラは、名前マングリングを使用して、「operator new」のエクスポート名である「?? 2 @ YAPAXI @ Z」のような本当に奇妙な名前を生成します。これは、関数のオーバーロードをサポートしているため、必要な悪でした。そしてもともとは、プログラムをビルドするためにレガシーC言語ツールを使用するプリプロセッサとして設計されていました。そのため、たとえば、void foo(char)オーバーロードとvoid foo(int)オーバーロードを異なる名前で区別する必要がありました。これは_extern "C"_構文が作用する場所であり、C++コンパイラーにnot名前変換を関数名に適用するように指示します。相互運用コードを記述するほとんどのプログラマーは、他の言語の宣言を記述しやすくするために意図的に使用します。これは実際には間違いですが、装飾は不一致を見つけるのに非常に役立ちます。リンカの.mapファイルまたはDumpbin.exe/exportsユーティリティを使用して、装飾名を確認します。 undname.exe SDKユーティリティは、マングルされた名前を元のC++宣言に戻すのに非常に便利です。

したがって、これでプロパティがクリアされます。 EntryPointを使用して、エクスポートされた関数の正確な名前を指定します。これは、特にC++のマングルされた名前の場合、独自のコードで呼び出すものとは一致しない場合があります。そして、ExactSpellingを使用して、すでに正しい名前を指定しているため、代替名を見つけようとしないようにピンボークマーシャラーに指示します。

私はしばらくの間、筆記のけいれんを看護します。質問のタイトルへの答えは明確である必要があります。Stdcallはデフォルトですが、CまたはC++で記述されたコードでは不一致です。また、[DllImport]宣言はnot互換ではありません。これにより、PInvokeStackImbalance Managed Debugger Assistantからのデバッガーで警告が生成されます。これは、不正な宣言を検出するように設計されたデバッガー拡張機能です。特にリリースビルドでは、かなりランダムにコードがクラッシュする可能性があります。 MDAをオフにしていないことを確認してください。

144
Hans Passant

cdeclstdcallは両方ともC++と.NETの間で有効かつ使用できますが、2つの管理されていない世界と管理された世界の間で一貫している必要があります。したがって、InvokedFunctionのC#宣言は無効です。 stdcallである必要があります。 MSDNサンプルは、stdcall(MessageBeep)とcdecl(printf)の2つの異なる例を示しています。それらは無関係です。

7
Simon Mourier