69

私は、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.CdeclC#の両方にあります。なぜそうではないのCallingConvention.Stdcallですか?または、さらに、なぜ__stdcallC ++にあるのですか?

前もって感謝します!

4

2 に答える 2

173

これはSOの質問で繰り返し出てきます、私はこれを(長い)参照回答に変えようとします。32ビットコードには、互換性のない呼び出し規約の長い歴史があります。昔は理にかなっていたが、今日のリアエンドではほとんどが大きな苦痛である関数呼び出しを行う方法の選択。64ビットコードの呼び出し規約は1つだけで、別の呼び出し規約を追加すると、南大西洋の小さな島に送られます。

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

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

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

  • __fastcallは、相互に互換性のない選択肢を持つ、かなり不十分に定義された呼び出し規約です。それは、かつてコンパイラ技術に非常に影響力を持っていた会社であるBorlandコンパイラでは、崩壊するまで一般的でした。また、C#で有名なAnders Hejlsbergを含む、多くのMicrosoft従業員の元雇用主でもあります。それらのいくつかをスタックではなくCPUレジスタに渡すことにより、引数の受け渡しをより安価にするために発明されました。標準化が不十分なため、マネージコードではサポートされていません。

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

  • __clrcallマネージコードの呼び出し規約です。これは他のもののブレンドであり、このポインタは__thiscallのように渡され、最適化された引数は__fastcallのように渡され、引数の順序は__cdeclのように渡され、呼び出し元のクリーンアップは__stdcallのように渡されます。マネージコードの大きな利点は、ジッターに組み込まれたベリファイアです。これにより、発信者と着信者の間に非互換性が生じることはありません。したがって、設計者はこれらすべての規則を利用できますが、手荷物を抱えることはありません。コードを安全にするオーバーヘッドにもかかわらず、マネージコードがネイティブコードとの競争力を維持する方法の例。

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

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

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

  • __cdecl関数の標準的な装飾は_fooです。単一の主要なアンダースコア。pinvokeマーシャラーはこれを自動的に分類します。残念ながら、__stdcallのオプションの@n接尾辞では、CallingConventionプロパティが間違っていることを通知できず、大きな損失が発生します。

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

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

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

于 2013-03-27T16:28:04.360 に答える
9

cdeclまたstdcall、C ++と.NETの間で有効かつ使用可能ですが、2つのアンマネージドワールドとマネージドワールドの間で一貫している必要があります。したがって、InvokedFunctionのC#宣言は無効です。stdcallである必要があります。MSDNサンプルは、2つの異なる例を示しています。1つはstdcall(MessageBeep)を使用し、もう1つはcdecl(printf)を使用しています。それらは無関係です。

于 2013-03-27T14:39:31.810 に答える