web-dev-qa-db-fra.com

Pourquoi les appels Cdecl sont-ils souvent incompatibles dans la convention "standard" P / Invoke?

Je travaille sur une base de code assez grande dans laquelle la fonctionnalité C++ est P/invoquée à partir de C #.

Il y a beaucoup d'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 parcouru le filet (dans la mesure où j'en suis capable) pour savoir pourquoi cet apparent décalage existe. Par exemple, pourquoi y a-t-il un Cdecl dans le C # et __stdcall dans le C++? Apparemment, cela entraîne l'effacement de la pile deux fois, mais, dans les deux cas, les variables sont poussées sur la pile dans le même ordre inverse, de sorte que je ne vois aucune erreur, bien que la possibilité que les informations de retour soient effacées en cas de tenter une trace pendant le débogage?

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

Encore une fois, il y a à la fois extern "C" dans le code C++ et CallingConvention.Cdecl dans le C #. Pourquoi n'est-ce pas CallingConvention.Stdcall? Ou, d'ailleurs, pourquoi y a-t-il __stdcall en C++?

Merci d'avance!

53
Kadaj Nakamura

Cela revient à plusieurs reprises dans les questions SO, je vais essayer de transformer cela en une (longue) réponse de référence. Le code 32 bits est sellé avec une longue histoire de conventions d'appel incompatibles. Choix sur la façon dont pour faire un appel de fonction qui avait du sens il y a longtemps mais qui est surtout une douleur géante à l'arrière aujourd'hui. Le code 64 bits n'a qu'une seule convention d'appel, celui qui va en ajouter une autre va être envoyé sur une petite île en l'Atlantique Sud.

Je vais essayer d'annoter cette histoire et leur pertinence au-delà de ce qui est dans le article Wikipedia . Le point de départ est que les choix à effectuer pour effectuer un appel de fonction sont l'ordre dans lequel passer les arguments, où stocker les arguments et comment nettoyer après l'appel.

  • __stdcall A trouvé sa place dans la programmation Windows grâce à l'ancienne convention d'appel Pascal 16 bits, utilisée dans Windows 16 et OS/2. C'est la convention utilisée par toutes les fonctions de l'API Windows ainsi que COM. Étant donné que la plupart des pinvoke étaient destinés à effectuer des appels au système d'exploitation, Stdcall est la valeur par défaut si vous ne le spécifiez pas explicitement dans l'attribut [DllImport]. Sa seule et unique raison d'existence est qu'elle précise que l'appelé se nettoie. Ce qui produit un code plus compact, très important à l'époque où ils devaient compresser un système d'exploitation GUI dans 640 kilo-octets de RAM. Son plus gros inconvénient est qu'il est dangereux . Un décalage entre ce que l'appelant suppose être les arguments d'une fonction et ce que l'appelé implémenté provoque un déséquilibre de la pile. Ce qui à son tour peut causer des diagnostics de crash extrêmement difficiles.

  • __cdecl Est la convention d'appel standard pour le code écrit en langage C. Sa principale raison d'existence est qu'il prend en charge les appels de fonction avec un nombre variable d'arguments. Commun en code C avec des fonctions comme printf () et scanf (). Avec pour effet secondaire que puisque c'est l'appelant qui sait combien d'arguments ont été réellement passés, c'est l'appelant qui nettoie. Oublier CallingConvention = CallingConvention.Cdecl dans la déclaration [DllImport] est un très bogue courant.

  • __fastcall Est une convention d'appel assez mal définie avec des choix mutuellement incompatibles. C'était courant dans les compilateurs Borland, une entreprise qui était autrefois très influente dans la technologie des compilateurs jusqu'à leur désintégration. Également l'ancien employeur de nombreux employés de Microsoft, dont Anders Hejlsberg de renommée C #. Il a été inventé pour rendre l'argument passant moins cher en passant certains d'entre eux via les registres CPU au lieu de la pile. Il n'est pas pris en charge dans le code managé en raison de la mauvaise standardisation.

  • __thiscall Est une convention d'appel inventée pour le code C++. Très similaire à __cdecl mais il spécifie également comment le pointeur caché ce pointeur pour un objet de classe est transmis aux méthodes d'instance d'une classe. Un détail supplémentaire en C++ au-delà de C. Bien qu'il semble simple à implémenter, le marshaller pinvoke .NET ne le prend pas en charge . Une raison majeure pour laquelle vous ne pouvez pas épingler du code C++. La complication n'est pas la convention d'appel, c'est la valeur correcte du pointeur this . Ce qui peut devenir très compliqué en raison de la prise en charge de l'héritage multiple par C++. Seul un compilateur C++ peut déterminer ce qui doit être passé exactement. Et uniquement 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 d'implémenter MI et de l'optimiser.

  • __clrcall Est la convention d'appel pour le code managé. C'est un mélange des autres, ce pointeur passant comme __thiscall, l'argument optimisé passant comme __fastcall, l'ordre des arguments comme __cdecl et le nettoyage de l'appelant comme __stdcall. Le grand avantage du code managé est le vérificateur intégré à la gigue. Ce qui garantit qu'il ne peut jamais y avoir d'incompatibilité entre l'appelant et l'appelé. Permettant ainsi aux concepteurs de profiter de toutes ces conventions mais sans le bagage d'ennuis. Un exemple de la façon dont le code managé pourrait rester compétitif avec le code natif malgré la surcharge de sécurité du code.

Vous mentionnez extern "C", Comprendre l'importance de cela est également important pour survivre à l'interaction. Les compilateurs de langage décorent souvent les noms des fonctions exportées avec des caractères supplémentaires. Également appelé "mutilation de nom". C'est une astuce assez merdique qui ne cesse de causer des problèmes. Et vous devez le comprendre pour déterminer les valeurs appropriées des propriétés CharSet, EntryPoint et ExactSpelling d'un attribut [DllImport]. Il existe de nombreuses conventions:

  • Décoration API Windows. Windows était à l'origine un système d'exploitation non Unicode, utilisant un codage 8 bits pour les chaînes. Windows NT a été le premier à devenir Unicode. Cela a causé un problème de compatibilité assez important, l'ancien code n'aurait pas pu fonctionner sur les nouveaux systèmes d'exploitation car il transmettrait des chaînes codées 8 bits aux fonctions winapi qui attendent une chaîne Unicode codée en utf-16. Ils ont résolu ce problème en écrivant deux versions de chaque fonction winapi. Un qui prend des chaînes 8 bits, un autre qui prend des chaînes Unicode. Et distingué entre les deux en collant la lettre A à la fin du nom de la version héritée (A = Ansi) et un W à la fin de la nouvelle version (W = large). Rien n'est ajouté si la fonction ne prend pas de chaîne. Le marshaller pinvoke gère cela automatiquement sans votre aide, il essaiera simplement de trouver les 3 versions possibles. Vous devez cependant toujours spécifier CharSet.Auto (ou Unicode), la surcharge de la fonction héritée traduisant la chaîne d'Ansi en Unicode est inutile et avec perte.

  • La décoration standard des fonctions __stdcall est _foo @ 4. Trait de soulignement en tête et un suffixe @n qui indique la taille combinée des arguments. Ce suffixe a été conçu pour aider à résoudre le problème de déséquilibre de pile désagréable si l'appelant et l'appelé ne s'entendent pas sur le nombre d'arguments. Fonctionne bien, bien que le message d'erreur ne soit pas grand, le marshaller pinvoke vous dira qu'il ne peut pas trouver le point d'entrée. Il convient de noter que Windows, lors de l'utilisation de __stdcall, n'utilise pas cette décoration. C'était intentionnel, donnant aux programmeurs une chance d'obtenir le bon argument GetProcAddress (). Le marshaller pinvoke s'occupe également de cela automatiquement, essayant d'abord de trouver le point d'entrée avec le suffixe @n, puis d'essayer celui sans.

  • La décoration standard de la fonction __cdecl est _foo. Un seul trait de soulignement. Le marshaller pinvoke trie cela automatiquement. Malheureusement, le suffixe @n facultatif pour __stdcall ne lui permet pas de vous dire que votre propriété CallingConvention est incorrecte, une grande perte.

  • Les compilateurs C++ utilisent le changement de nom, produisant des noms vraiment bizarres comme "?? 2 @ YAPAXI @ Z", le nom exporté pour "opérateur nouveau". C'était un mal nécessaire en raison de son support pour la surcharge de fonctions. Et à l'origine, il avait été conçu comme un préprocesseur qui utilisait les outils du langage C hérités pour obtenir le programme. Ce qui obligeait à distinguer, disons, une surcharge void foo(char) et void foo(int) en leur donnant des noms différents. C'est là que la syntaxe extern "C" Entre en jeu, elle indique au compilateur C++ de ne pas appliquer le nom mangling au nom de la fonction. La plupart des programmeurs qui écrivent du code d'interopérabilité l'utilisent intentionnellement pour rendre la déclaration dans l'autre langue plus facile à écrire. Ce qui est en fait une erreur, la décoration est très utile pour rattraper les décalages. Vous utiliseriez le fichier .map de l'éditeur de liens ou l'utilitaire Dumpbin.exe/exports pour voir les noms décorés. L'utilitaire undname.exe SDK est très pratique pour reconvertir un nom modifié à sa déclaration C++ d'origine.

Cela devrait donc clarifier les propriétés. Vous utilisez EntryPoint pour donner le nom exact de la fonction exportée, celle qui peut ne pas correspondre à ce que vous voulez appeler dans votre propre code, en particulier pour les noms modifiés C++. Et vous utilisez ExactSpelling pour dire au marshaller pinvoke de ne pas essayer de trouver les noms alternatifs car vous avez déjà donné le nom correct.

Je vais soigner ma crampe d'écriture depuis un moment maintenant. La réponse au titre de votre question doit être claire, Stdcall est la valeur par défaut, mais il ne correspond pas au code écrit en C ou C++. Et votre déclaration [DllImport] n'est pas compatible. Cela devrait produire un avertissement dans le débogueur à partir de l'assistant de débogage géré PInvokeStackImbalance, une extension de débogueur conçue pour détecter les déclarations incorrectes. Et peut planter votre code de manière aléatoire, en particulier dans la version Release. Assurez-vous que vous n'avez pas éteint le MDA.

144
Hans Passant

cdecl et stdcall sont à la fois valides et utilisables entre C++ et .NET, mais ils doivent être cohérents entre les deux mondes non managé et managé. Votre déclaration C # pour InvokedFunction n'est donc pas valide. Devrait être stdcall. L'exemple MSDN ne donne que deux exemples différents, l'un avec stdcall (MessageBeep) et l'autre avec cdecl (printf). Ils ne sont pas liés.

7
Simon Mourier