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!
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.
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.