Warum stimmen Cdecl-Aufrufe oft nicht mit der “Standard”-P/Invoke-Konvention überein?

Lesezeit: 10 Minuten

Ich arbeite an einer ziemlich großen Codebasis, in der die C++-Funktionalität P/Invoked from C# ist.

Es gibt viele Aufrufe in unserer Codebasis wie…

C++:

extern "C" int __stdcall InvokedFunction(int);

Mit entsprechendem C#:

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

Ich habe das Netz (soweit ich dazu in der Lage bin) nach der Begründung durchforstet, warum diese offensichtliche Diskrepanz besteht. Warum gibt es beispielsweise Cdecl in C# und __stdcall in C++? Anscheinend führt dies dazu, dass der Stack zweimal gelöscht wird, aber in beiden Fällen werden Variablen in derselben umgekehrten Reihenfolge auf den Stack geschoben, sodass ich keine Fehler sehe, obwohl die Möglichkeit besteht, dass Rückgabeinformationen im Falle von gelöscht werden Versuchen Sie eine Ablaufverfolgung während des Debuggens?

Von 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);

Wieder einmal gibt es beides extern "C" im C++-Code und CallingConvention.Cdecl in der C#. Warum nicht CallingConvention.Stdcall? Oder darüber hinaus, warum gibt es __stdcall in C++?

Vielen Dank im Voraus!

Warum stimmen Cdecl Aufrufe oft nicht mit der Standard PInvoke Konvention uberein
Hans Passanten

Dies kommt wiederholt in SO-Fragen vor, ich werde versuchen, daraus eine (lange) Referenzantwort zu machen. 32-Bit-Code ist mit einer langen Geschichte inkompatibler Aufrufkonventionen behaftet. Entscheidungen darüber, wie man einen Funktionsaufruf macht, waren vor langer Zeit sinnvoll, sind heute aber meist ein riesiger Schmerz im hinteren Ende. 64-Bit-Code hat nur eine Aufrufkonvention, wer eine weitere hinzufügt, wird auf eine kleine Insel im Südatlantik geschickt.

Ich werde versuchen, diese Geschichte und ihre Relevanz über das hinaus zu kommentieren, was in der enthalten ist Wikipedia-Artikel. Ausgangspunkt ist, dass die Entscheidungen, die beim Ausführen eines Funktionsaufrufs getroffen werden müssen, die Reihenfolge sind, in der die Argumente übergeben werden, wo die Argumente gespeichert werden und wie nach dem Aufruf bereinigt wird.

  • __stdcall fand seinen Weg in die Windows-Programmierung durch die alte 16-Bit-Pascal-Aufrufkonvention, die in 16-Bit-Windows und OS/2 verwendet wurde. Es ist die Konvention, die von allen Windows-API-Funktionen sowie von COM verwendet wird. Da die meisten Pinvokes für Betriebssystemaufrufe gedacht waren, ist Stdcall der Standardwert, wenn Sie es nicht explizit in der angeben [DllImport] Attribut. Sein einziger Existenzgrund ist, dass er angibt, dass der Aufgerufene aufräumt. Dadurch entsteht kompakterer Code, was damals sehr wichtig war, als man ein GUI-Betriebssystem in 640 Kilobyte RAM quetschen musste. Sein größter Nachteil ist, dass es so ist gefährlich. Eine Diskrepanz zwischen dem, was der Aufrufer als Argumente für eine Funktion annimmt, und dem, was der Aufgerufene implementiert hat, führt dazu, dass der Stapel unausgeglichen wird. Was wiederum extrem schwer zu diagnostizierende Abstürze verursachen kann.

  • __cdecl ist die Standard-Aufrufkonvention für Code, der in der Sprache C geschrieben ist. Sein Hauptgrund für seine Existenz ist, dass es Funktionsaufrufe mit einer variablen Anzahl von Argumenten unterstützt. Häufig in C-Code mit Funktionen wie printf() und scanf(). Mit dem Nebeneffekt, dass der Aufrufer aufräumt, da er weiß, wie viele Argumente tatsächlich übergeben wurden. Vergessen von CallingConvention = CallingConvention.Cdecl in der [DllImport] Deklaration ist ein sehr allgemeiner Fehler.

  • __fastcall ist eine ziemlich schlecht definierte Aufrufkonvention mit gegenseitig inkompatiblen Optionen. Es war bei Borland-Compilern üblich, einem Unternehmen, das einst sehr einflussreich in der Compiler-Technologie war, bis sie sich auflösten. Auch der ehemalige Arbeitgeber vieler Microsoft-Mitarbeiter, darunter Anders Hejlsberg von C#. Es wurde erfunden, um das Vorbeigehen billiger zu machen etwas von ihnen durch CPU-Register anstelle des Stacks. Aufgrund der schlechten Standardisierung wird es in verwaltetem Code nicht unterstützt.

  • __thiscall ist eine Aufrufkonvention, die für C++-Code erfunden wurde. Sehr ähnlich zu __cdecl, aber es gibt auch an, wie die versteckten Das Zeiger auf ein Klassenobjekt wird an Instanzmethoden einer Klasse übergeben. Ein zusätzliches Detail in C++ jenseits von C. Während es einfach zu implementieren aussieht, tut es der .NET-Pinvoke-Marshaller nicht unterstütze es. Ein Hauptgrund dafür, dass C++-Code nicht per Pinvoke ausgeführt werden kann. Die Komplikation ist nicht die Berufungskonvention, sie ist der eigentliche Wert der Das Zeiger. Was aufgrund der Unterstützung von C++ für Mehrfachvererbung sehr kompliziert werden kann. Nur ein C++-Compiler kann jemals herausfinden, was genau übergeben werden muss. Und nur der exakt gleiche C++-Compiler, der den Code für die C++-Klasse generiert hat, verschiedene Compiler haben unterschiedliche Entscheidungen darüber getroffen, wie MI implementiert und optimiert werden soll.

  • __clrcall ist die Aufrufkonvention für verwalteten Code. Es ist eine Mischung aus den anderen, Das Zeigerübergabe wie __thiscall, optimierte Argumentübergabe wie __fastcall, Argumentreihenfolge wie __cdecl und Aufruferbereinigung wie __stdcall. Der große Vorteil von verwaltetem Code ist die Prüfer in den Jitter eingebaut. Dadurch wird sichergestellt, dass es niemals zu einer Inkompatibilität zwischen Anrufer und Angerufenem kommen kann. Auf diese Weise können die Designer die Vorteile all dieser Konventionen nutzen, jedoch ohne Probleme. Ein Beispiel dafür, wie verwalteter Code trotz des Aufwands, Code sicher zu machen, mit nativem Code konkurrenzfähig bleiben könnte.

Du erwähnst extern "C", dessen Bedeutung zu verstehen, ist ebenfalls wichtig, um Interop zu überleben. Sprache Compiler oft schmücken die Namen der exportierten Funktion mit zusätzlichen Zeichen. Auch „Namensverfälschung“ genannt. Es ist ein ziemlich beschissener Trick, der nie aufhört, Ärger zu verursachen. Und Sie müssen es verstehen, um die richtigen Werte der CharSet-, EntryPoint- und ExactSpelling-Eigenschaften von a zu bestimmen [DllImport] Attribut. Es gibt viele Konventionen:

  • Windows-API-Dekoration. Windows war ursprünglich ein Nicht-Unicode-Betriebssystem, das 8-Bit-Codierung für Zeichenfolgen verwendete. Windows NT war das erste, das im Kern Unicode wurde. Dies verursachte ein ziemlich großes Kompatibilitätsproblem, alter Code hätte nicht auf neuen Betriebssystemen ausgeführt werden können, da er 8-Bit-codierte Zeichenfolgen an Winapi-Funktionen übergeben würde, die eine utf-16-codierte Unicode-Zeichenfolge erwarten. Sie haben dies schriftlich gelöst zwei Versionen jeder winapi-Funktion. Einer, der 8-Bit-Strings akzeptiert, ein anderer, der Unicode-Strings akzeptiert. Und zwischen den beiden unterschieden, indem der Buchstabe A an das Ende des Namens der Legacy-Version (A = Ansi) und ein W an das Ende der neuen Version (W = wide) geklebt wird. Es wird nichts hinzugefügt, wenn die Funktion keinen String akzeptiert. Der Pinvoke-Marshaller erledigt dies automatisch ohne Ihre Hilfe, er versucht einfach, alle 3 möglichen Versionen zu finden. Sie sollten jedoch immer CharSet.Auto (oder Unicode) angeben, der Overhead der Legacy-Funktion, die den String von Ansi nach Unicode übersetzt, ist unnötig und verlustbehaftet.

  • Die Standarddekoration für __stdcall-Funktionen ist _foo@4. Führender Unterstrich und ein @n-Postfix, das die kombinierte Größe der Argumente angibt. Dieses Postfix wurde entwickelt, um das unangenehme Problem des Stapelungleichgewichts zu lösen, wenn Aufrufer und Aufgerufener sich nicht über die Anzahl der Argumente einig sind. Funktioniert gut, obwohl die Fehlermeldung nicht großartig ist, teilt Ihnen der Pinvoke-Marshaller mit, dass er den Einstiegspunkt nicht finden kann. Bemerkenswert ist, dass Windows dies bei der Verwendung von __stdcall tut nicht Verwenden Sie diese Dekoration. Das war beabsichtigt, um Programmierern eine Chance zu geben, das GetProcAddress()-Argument richtig hinzubekommen. Der Pinvoke-Marshaller kümmert sich auch darum automatisch, indem er zuerst versucht, den Einstiegspunkt mit dem @n-Postfix zu finden, als nächstes den ohne.

  • Die Standarddekoration für die __cdecl-Funktion ist _foo. Ein einzelner führender Unterstrich. Der Pinvoke-Marshaller sortiert dies automatisch aus. Leider erlaubt das optionale @n-Postfix für __stdcall nicht, Ihnen mitzuteilen, dass Ihre CallingConvention-Eigenschaft falsch ist, großer Verlust.

  • C++-Compiler verwenden Name Mangling und erzeugen wirklich bizarr aussehende Namen wie “??2@YAPAXI@Z”, den exportierten Namen für “operator new”. Dies war aufgrund der Unterstützung für Funktionsüberladung ein notwendiges Übel. Und es war ursprünglich als Präprozessor konzipiert, der ältere C-Sprachwerkzeuge verwendete, um das Programm zu erstellen. Was es notwendig machte, zu unterscheiden zwischen, sagen wir, a void foo(char) und ein void foo(int) überladen, indem Sie ihnen unterschiedliche Namen geben. Hier ist die extern "C" Syntax ins Spiel kommt, weist sie den C++-Compiler an nicht Wenden Sie die Namensverfälschung auf den Funktionsnamen an. Die meisten Programmierer, die Interop-Code schreiben, verwenden ihn absichtlich, um das Schreiben der Deklaration in der anderen Sprache zu vereinfachen. Was eigentlich ein Fehler ist, die Dekoration ist sehr nützlich, um Fehlpaarungen zu erkennen. Sie würden die .map-Datei des Linkers oder das Dienstprogramm Dumpbin.exe /exports verwenden, um die ergänzten Namen anzuzeigen. Das SDK-Dienstprogramm undname.exe ist sehr praktisch, um einen verstümmelten Namen wieder in seine ursprüngliche C++-Deklaration umzuwandeln.

Dies sollte also die Eigenschaften klären. Sie verwenden EntryPoint, um den genauen Namen der exportierten Funktion anzugeben, der möglicherweise nicht gut zu dem passt, was Sie in Ihrem eigenen Code nennen möchten, insbesondere für C++-verstümmelte Namen. Und Sie verwenden ExactSpelling, um dem Pinvoke-Marshaller mitzuteilen, dass er nicht versuchen soll, die alternativen Namen zu finden, weil Sie bereits den richtigen Namen angegeben haben.

Ich werde meinen Schreibkrampf jetzt eine Weile stillen. Die Antwort auf den Titel Ihrer Frage sollte klar sein, Stdcall ist die Standardeinstellung, passt jedoch nicht zu Code, der in C oder C++ geschrieben wurde. Und dein [DllImport] Deklaration ist nicht kompatibel. Dies sollte im Debugger eine Warnung vom PInvokeStackImbalance Managed Debugger Assistant erzeugen, einer Debuggererweiterung, die entwickelt wurde, um fehlerhafte Deklarationen zu erkennen. Und kann Ihren Code eher zufällig zum Absturz bringen, insbesondere im Release-Build. Stellen Sie sicher, dass Sie den MDA nicht ausgeschaltet haben.

  • Danke für die Lektion. Ich habe einen neuen Respekt vor P/Invoke. Und ich schätze __clrcall und das Fehlen von Mehrfachvererbung in C# besser.

    – Jacob Foshee

    27. März 2013 um 16:51 Uhr

  • +1 Ich habe ein paar leicht hängende Kommentare. Du sprichst über __fastcall aber das ist eigentlich eine MS-Anrufkonvention. Allgemein kann es als Teil der Familie der Fastcall-Konventionen betrachtet werden. MS Fastcall, Borland Fastcall usw. Die MS-Version, __fastcall verwendet nur zwei x86-Register: ECX, EDX. Die Borland-Version, die heute hier in der Delphi-Welt als die weiterlebt register Konvention verwendet drei Register. EAX wird der Mischung hinzugefügt.

    – David Heffernan

    27. März 2013 um 17:37 Uhr

  • Mein anderer Kommentar bezieht sich auf die Namensdekoration. Ich habe kürzlich bei der Beantwortung einer Frage hier entdeckt, dass verschiedene Compiler unterschiedliche Dekorationen für haben __stdcall. Es scheint, dass MS-, Borland- und GNU-Toolsets sich alle unterscheiden. Ich vermute, dass andere Compiler noch mehr Variationen einführen.

    – David Heffernan

    27. März 2013 um 17:39 Uhr

  • Hans, danke für diese Erklärung! Ich konnte nur Kleinigkeiten sammeln, indem ich unzählige Punkte durchgesehen habe. Es ist ausgezeichnet, all diese Informationen an einem Ort zu haben! Auch danke, Leute, für die Kommentare dazu! Mir fällt das alles noch ein, aber gerade im Hinblick auf die Historie ist das sehr hilfreich, warum sich die Dinge in dieser jetzigen Wirrwarr/Mischprobe befinden!

    – Kadaj Nakamura

    28. März 2013 um 3:37 Uhr

  • Das ist toll. All diese Informationen habe ich noch nie an einem Ort gesehen. Ich wünschte, ich könnte dieser Antwort mehrere Upvotes geben.

    – Agentlien

    29. März 2013 um 20:21 Uhr

cdecl und stdcall zwischen C++ und .NET gültig und verwendbar sind, aber zwischen den beiden nicht verwalteten und verwalteten Welten konsistent sein sollten. Ihre C#-Deklaration für InvokedFunction ist also ungültig. Sollte stdcall sein. Das MSDN-Beispiel enthält nur zwei verschiedene Beispiele, eines mit stdcall (MessageBeep) und eines mit cdecl (printf). Sie sind nicht verwandt.

  • Einverstanden; empfehlen, etwas über “Anrufkonventionen” zu recherchieren, um zu sehen, warum der Unterschied wichtig ist.

    – JerKimball

    27. März 2013 um 15:36 Uhr

993210cookie-checkWarum stimmen Cdecl-Aufrufe oft nicht mit der “Standard”-P/Invoke-Konvention überein?

This website is using cookies to improve the user-friendliness. You agree by using the website further.

Privacy policy