Pourquoi Cdecl appelle souvent incompatibles dans la “norme” de P/Invoke Convention?
Je suis en train de travailler sur une assez grande base de code en C++ fonctionnalité est P/Invoquée à partir de C#.
Il y a de nombreux appels dans notre base de code, tels que...
C++:
extern "C" int __stdcall InvokedFunction(int);
Avec un C# correspondant:
[DllImport("CPlusPlus.dll", ExactSpelling = true, SetLastError = true, CallingConvention = CallingConvention.Cdecl)]
private static extern int InvokedFunction(IntPtr intArg);
J'ai écumé le net (dans la mesure où je suis capable) pour le raisonnement pour expliquer pourquoi cette apparente incompatibilité existe. Par exemple, pourquoi est-il Cdecl dans le C#, et __ _ _ stdcall dans le C++? Apparemment, cette résultats dans la pile étant éliminé à deux reprises, mais, dans les deux cas, les variables sont poussés sur la pile dans le même ordre inverse, de telle sorte que je ne vois pas d'erreurs, mais la possibilité que le retour d'information est désactivée en cas de tentative de trace au cours de débogage?
À partir de 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);
Une fois de plus, il est à la fois extern "C"
dans le code C++, et CallingConvention.Cdecl
dans le C#. Pourquoi n'est-il pas CallingConvention.Stdcall
? Ou, d'ailleurs, pourquoi est-il __stdcall
dans le C++?
Merci d'avance!
Vous devez vous connecter pour publier un commentaire.
Cela revient à plusieurs reprises dans les questions, je vais essayer d'en faire un (long) référence de réponse. Code 32 bits est aux prises avec une longue histoire de l'incompatibilité des conventions d'appel. Des choix sur la façon de faire un appel de fonction qui fait sens il y a longtemps, mais sont pour la plupart d'un géant de la douleur dans l'arrière aujourd'hui. Code 64 bits n'a qu'une convention d'appel, qui va ajouter un autre que l'on va obtenir envoyé à la petite île de l'Atlantique Sud.
Je vais essayer de les annoter que l'histoire et la pertinence d'entre eux au-delà de ce qui est dans le Article de Wikipedia. Le point de départ est que les choix à faire dans la façon de faire un appel de fonction sont à l'ordre dans lequel passer des arguments, où stocker les arguments et la façon de le nettoyage après l'appel.
__stdcall
trouvé sa place dans la programmation de Windows par le biais de la olden 16 bits pascal convention d'appel, utilisée en 16-bits de Windows et OS/2. C'est la convention utilisée par toutes les fonctions de l'api Windows ainsi que de la COM. Depuis plus pinvoke était destiné à rendre les OS des appels, Stdcall est la valeur par défaut si vous ne spécifiez pas explicitement dans l' [DllImport] attribut. Sa seule et unique raison d'être est qu'il indique que le destinataire de l'appel nettoie. Qui produit du code plus compact, très important à l'époque où ils devaient se frayer un interface graphique du système d'exploitation en 640 kilo-octets de RAM. Son plus grand inconvénient est qu'il est dangereux. Un décalage entre ce que l'appelant n'assume sont les arguments d'une fonction et que le destinataire de l'appel mis en œuvre des causes de la pile pour obtenir déséquilibrée. Qui à son tour peut causer extrêmement difficile à diagnostiquer les pannes.__cdecl
est la convention d'appel standard pour le code écrit dans le langage C. Sa première raison de l'existence, c'est qu'il prend en charge les appels de fonctions avec un nombre variable d'arguments. Commune dans le code C, avec des fonctions comme printf() et scanf(). Avec l'effet secondaire, puisque c'est l'appelant qui sait, de nombreux arguments ont été réellement passé, c'est l'appelant qui nettoie. Oublier CallingConvention = CallingConvention.Cdecl dans l' [DllImport] déclaration est un très commune de bug.__fastcall
est assez mal définie convention d'appel avec mutuellement incompatibles choix. C'était courant dans les compilateurs Borland, une société à la fois très influents dans le compilateur de la technologie jusqu'à ce qu'ils s'effondrent. Aussi à l'ancien employeur de nombreux employés de Microsoft, y compris Anders Hejlsberg de C# renommée. Il a été inventé pour rendre argument de passage moins cher en passant certains par le biais de registres du CPU au lieu de la pile. Il n'est pas pris en charge dans le code managé en raison de la mauvaise normalisation.__thiscall
est une convention d'appel a inventé pour le code C++. Très semblable à l' __cdecl, mais il précise également comment le caché ce pointeur d'un objet de classe est passé à des méthodes d'instance d'une classe. Un détail supplémentaire en C++ au-delà de C. bien qu'il semble simple à mettre en oeuvre, les .NET pinvoke marshaller ne pas le soutenir. L'une des principales raisons qui vous ne pouvez pas pinvoke code C++. La complication n'est pas la convention d'appel, c'est la valeur correcte de la ce pointeur. Qui peut devenir très compliquée en raison de C++de soutien pour l'héritage multiple. Seulement un compilateur C++ peut jamais comprendre ce qu'est exactement doit être passé. Et seulement exactement le même compilateur C++ qui a généré le code de la classe C++, différents compilateurs ont fait des choix différents sur la façon de mettre en œuvre des MI et comment l'optimiser.__clrcall
est la convention d'appel pour le code managé. C'est un mélange des autres, ce pointeur de passer __thiscall, optimisé argument passant comme des __fastcall, un argument d'ordre comme __cdecl et identification de nettoyage comme __stdcall. Le grand avantage de code managé est le vérificateur construit dans le jitter. Qui fait en sorte qu'il ne peut jamais être une incompatibilité entre l'appelant et l'appelé. Permettant ainsi aux concepteurs de prendre les avantages de l'ensemble de ces conventions, mais sans les bagages de difficulté. Un exemple de code managé pourrait rester compétitif avec du code natif en dépit de la surcharge de rendre le code sécurité.Vous mentionnez
extern "C"
, la compréhension de la signification de ce qui est important aussi bien pour survivre à l'interopérabilité. Langue compilateurs souvent décorer les noms de fonction exportée par des caractères supplémentaires. Aussi appelé "name mangling". C'est un joli truc de merde qui ne s'arrête jamais de causer des ennuis. Et vous avez besoin de comprendre pour déterminer les valeurs appropriées de la table de caractères, point d'entrée et ExactSpelling propriétés d'un [DllImport] attribut. Il existe de nombreuses conventions:Api Windows décoration. Windows a été à l'origine d'une non-Unicode système d'exploitation à l'aide de 8 bits codant pour les chaînes. Windows NT a été le premier qui devint Unicode à sa base. Qui a causé une assez grands problème de compatibilité, de l'ancien code n'aurait pas été capable de courir sur les nouveaux systèmes d'exploitation, depuis il passera à 8 bits chaînes codées à winapi fonctions que s'attendre à une codé en utf-16 chaîne Unicode. Ils ont résolu ce problème en écrivant deux versions de chaque winapi fonction. Celui qui prend la 8-bits de cordes, un autre qui prend des chaînes Unicode. Et la distinction entre les deux par le collage de la lettre A à la fin du nom de l'ancienne version (A = Ansi) et un W à la fin de la nouvelle version (W = large). Rien n'est ajouté si la fonction ne prend pas une chaîne de caractères. Pinvoke marshaller gère cela automatiquement sans votre aide, il sera tout simplement essayer de trouver tous les 3 versions possibles. Vous devez cependant toujours spécifier le jeu de caractères.Auto (ou Unicode), les frais généraux de l'héritage de la fonction de la traduction de la chaîne de l'Ansi en Unicode est inutile et avec perte.
La décoration standard pour __stdcall fonctions est _foo@4. Trait de soulignement et d'un @n suffixe qui indique la taille combinée des arguments. Cette postfix a été conçu pour aider à résoudre le méchant pile problème de déséquilibre si l'appelant et l'appelé ne s'entendent pas sur le nombre d'arguments. Fonctionne bien, même si le message d'erreur n'est pas très grande, pinvoke marshaller vous dira qu'il ne peut pas trouver le point d'entrée. Remarquable, c'est que les Fenêtres, tout en utilisant __stdcall, ne pas utiliser cette décoration. C'était volontaire, pour donner aux développeurs une chance de faire la GetProcAddress() argument de droit. Pinvoke marshaller prend également soin de cela automatiquement, en essayant d'abord de trouver le point d'entrée avec le @n postfix, à côté essayer celui sans.
La décoration standard pour __cdecl fonction est _foo. Un seul trait de soulignement. Pinvoke marshaller sortes ceci automatiquement. Malheureusement, l'option @n postfix pour __stdcall ne lui permet pas de vous dire que votre CallingConvention propriété est faux, grande perte.
Compilateurs C++ utilisez le nom d'amputation, la production de vraiment étrange à la recherche des noms comme "??2@YAPAXI@Z", le nom exporté pour "opérateur de nouveau". C'était un mal nécessaire en raison de son soutien pour la fonction de surcharge. Et il à l'origine ayant été conçu comme un préprocesseur qui ont utilisé l'héritage du langage C, de l'outillage pour obtenir le programme intégré. D'où la nécessité de distinguer entre, disons, un
void foo(char)
et unvoid foo(int)
surcharge de travail en leur donnant des noms différents. C'est là que leextern "C"
syntaxe entre en jeu, il indique au compilateur C++ pour pas appliquer le nom d'une déformation du nom de la fonction. La plupart des programmeur qui écrivent interop code intentionnellement utiliser pour en faire la déclaration dans l'autre langue plus facile à écrire. Qui est en fait une erreur, la décoration est très utile pour attraper les inadéquations. Vous pouvez utiliser l'éditeur de liens .fichier de la carte ou de la Dumpbin.exe /exportations de l'utilitaire pour voir les noms décorés. L'undname.exe utilitaire du kit de développement est très pratique pour convertir une déformation du nom d'origine de déclaration C++.Ce qui devrait éclaircir les propriétés. Vous utilisez le point d'entrée de donner le nom exact de la fonction exportée, l'une qui peut ne pas être un bon match pour ce que vous voulez l'appeler, dans votre propre code, en particulier pour C++ noms déformés. Et vous utilisez ExactSpelling dire pinvoke marshaller de ne pas essayer de trouver les noms de remplacement parce que vous avez déjà donné le nom correct.
Je vais l'infirmière de mon écriture crampe pendant un certain temps maintenant. La réponse à votre question, le titre doit être clair, Stdcall est la valeur par défaut, mais un décalage de code écrit en C ou C++. Et votre [DllImport] déclaration est pas compatible. Cela devrait produire un avertissement dans le débogueur du PInvokeStackImbalance Géré Débogueur Adjoint, une extension du débogueur qui a été conçu pour détecter les fausses déclarations. Et peut plutôt au hasard un crash de votre code, en particulier dans la version Release. Assurez-vous de ne pas tourner la MDA off.
__fastcall
mais qui est en fait un MS convention d'appel. De manière générique, il peut être considéré comme faisant partie de la famille de fastcall conventions. MS fastcall, Borland fastcall etc. La MME version,__fastcall
utilise seulement deux x86 registres: ECX, EDX. Borland version, qui vit aujourd'hui ici dans Delphi monde comme leregister
convention, utilise les trois registres. EAX est ajouté au mélange.__stdcall
. Il semble que MS, Borland et de l'ensemble des outils GNU sont tous différents. Je soupçonne que d'autres compilateurs introduire encore plus de variation.__vectorcall
!__vectorcall
après tout? Je suis actuellement en train de travailler à un projet avec un vecteur intrinsèques et nous avons tout simplement passer des vecteurs par (const) référence... Est-il de la documentation sur ce en dehors de MSDN? (C'est pas encore pour exemple dans Agner de la Brume d'excellents guides...)__fastcall
, et/ou est considéré comme suffisamment proches pour être appelé un. Vous ne savez pas si cela est correct ou non, mais, c'est à peu près entièrement basé sur les informations de tiers.?<name>@[<all containing scopes, right to left, each ending with '@'>]@<access modifier #><type><cv-qualifier>
. Les fonctions sont?<name>@[<all containing scopes, r-to-l, each ending with '@'>]@<access modifier & near/far><calling convention><ret type><param list><throw specifier>
. [Toutes les fonctions sontnear
en 32 ou 64 bits environnements.] Les modèles sont ...du charabia.int a;
est?a@@3HA
, etvoid NS::func();
est?func@NS@@YAXXZ
. Si un nom (y compris la résiliation de son '@') est rencontré à de multiples reprises, les utilisations suivantes sont remplacés par un chiffre (par exemplevoid NS::NS::NS()
est?NS@00@YAXXZ
). Contenant de la classe & espaces de noms sont traités de manière identique. En fonction des listes de paramètres, de caractères multi-<type>
codes sont abrégés comme les noms, étant remplacé par un chiffre; le type de retour n'est pas considérée comme de type 0. Seuls les 10 premiers noms et les 10 premiers types de paramètres sont abrégées, comme0..9
.N
, il est encodé commeN - 1
si0 < N < 11
; sinon, c'est codé comme un nombre hexadécimal, où0..F
sont remplacés parA..P
, et terminé par@
. Les nombres négatifs commencer avec?
, je crois.<cv-qualified pointer type>[<modifiers>]<target cv-qualifier><target type>
. Pointeurs de fonction ont la fonction de la déclaration du charabia (commeYAXXZ
) comme leur<target type>
, sauf avec<access modifier & near/far>
qu'un chiffre (généralement6
pour les non-membres &8
pour les membres), & ne pas avoir<target cv-qualifier>
. Pointeur Global objets sont difficiles à faire; leur fuite<cv-qualifier>
est une copie exacte de<target cv-qualifier>
, apparemment, y compris certains (ou tous?) de la possible<modifiers>
, si elle est présente.<target cv-qualifier>
.) Les pointeurs-à-membres ont modifié<target cv-qualifier>
élément, qui se compose de<target cv-qualifier letter (shifted)><containing class name>
, où<containing class name>
est le contenant de la classe de nom pleinement qualifié (avec chaque nom est arrêté avec@
, tous les champs d'interrogation de droite à gauche, et le nom entier est arrêté avec un supplément de@
); for global de pointeur vers des objets membres, le nom de la classe dans la fuite<cv-qualifer>
sera abrégée.?
(pour "pas un pointeur/référence") comme<cv-qualified pointer type>
. Les paramètres de la fonction, à la différence des objets globaux, n'ont pas une fuite<cv-qualifier>
élément, ce que je crois être la raison pointeur de symboles contiennent<target cv-qualifier>
; comme mentionné, multi-types de caractères sont abrégées après s'être rencontrés. (Par exemple,void f(int, int);
est?f@@YAXHH@Z
, maisvoid g(bool, bool)
est?g@@YAX_N0@Z
.)X
(pour(void)
). si elle se termine par des points de suspension (...
), les points de suspension est codé commeZ
. Si cela n'est pas vrai, la liste des paramètres est terminée avec@
. (Par exemple,void f()
est?f@@YAXXZ
,void g(int, ...)
est?g@@YAXHZZ
, etvoid h(int)
est?h@@YAXH@Z
.) Le<throw specifier>
apparemment suit les mêmes règles que le paramètre de la liste; toutefois, Visual Studio ignore tous lancer les prescripteurs, et les encode commethrow(...)
, c'est pourquoi tous les symboles de fonction fin deZ
.int f()
est?f@@YAHXZ
,const int g()
est?g@@YA?BHXZ
, etCL h()
, oùCL
est une classe, est?h@@YA?AVCL@@XZ
.$$F
pour les fonctions normales (par exemple,?f@@$$FYAXXZ
pour une version gérée devoid f()
),$$H
pourmain()
(pas sûr si cela est une toute autre signification), et$$J<number>
pourextern "C"
fonctions, où<number>
semble être fondée sur la convention d'appel. Pas sûr de tous les autres grands changements.<this cv-qualifier>
, entre<access modifier & near/far>
et<calling convention>
pour indiquer des informations sur lesthis
pointeur. Par exemple, pour la classeCL
,void CL::f()
est?f@CL@@QAEXXZ
,void CL::g() const
est?g@CL@@QBEXXZ
, etvoid CL::h() volatile
est?h@CL@@QCEXXZ
.this
pointeur ref-les qualificatifs sont également codées, mais je ne suis pas sûr exactement comment encore.CL
, avec des fonctions membres publiquesvoid ffff()
,void func() &
, etvoid func() &&
, le pointeur suivant-à-types de membres sont générés:P8CL@@AEXXZ
(void (__thiscall CL::*)(void)
),P8CL@@GAEXXZ
(void (__thiscall CL::*)(void)&
), etP8CL@@HAEXXZ
(void (__thiscall CL::*)(void)&&
).__based
de la propriété, et quelques autres propriétés, qui peuvent modifier les fonctions et/ou des pointeurs. Je ne suis pas sûr de la façon de les lire encore. Je ne suis pas sûr de la façon de lire des modèles encore.UnDecorateSymbolName()
, le plus puissant caché__unDName()
, et tout ce qui dépend de l'un des deux (commeUNDNAME
etDUMPBIN /SYMBOLS
), d'avoir des problèmes demangling tout pointeur global de l'objet lorsque le pointeur de cible ou de cv qualifiés. Plus précisément, il ne sait pas comment analyser la fuite<cv-qualifier>
correctement pour pointeur objets, et les sorties de la cible cv-qualificatifs à la fois le pointeur et la cible cv-qualificatifs.undname 0x2000 PAH
,undname 0x2000 QAH
, etundname 0x2000 PBH
sont correctement analysés commeint *
,int * const
, etint const *
, respectivement. [Où0x2000
est un indicateur pour direundname
qu'il soit donné une crue type de symbole, non déformés nom de symbole.] Cependant, si nous essayons deundname ?a@@3PAHA
,undname ?a@@3QAHA
, etundname ?a@@3PBHB
, c'sortiesint * a
,int * a
, etint const * const a
. Test avec?a@@3PBH?
et?a@@3?AHA
révèle que pour un plein de déformation de nom, il traite de la fuite<cv-qualifier>
élément que le pointeur de la cv-qualificatifs.?
que le type de pointeur, lors de cv-qualificatifs ne sont pas omis).cdecl
etstdcall
sont à la fois valables et utilisables entre le C++ et .NET, mais ils devraient cohérent entre les deux gérés et géré mondes. Si votre C# déclaration pour InvokedFunction est pas valide. Devrait être stdcall. La MSDN échantillon donne juste deux exemples, l'un avec stdcall (MessageBeep), et un avec cdecl (printf). Ils ne sont pas liés.