Pourquoi les appels cdecl sont-ils souvent incompatibles avec la convention P / Invoke «standard»?

Je travaille sur une base de code assez grande dans laquelle la fonctionnalité C ++ est P / Invoked from 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 net (dans la mesure où j’en suis capable) pour trouver la raison de l’existence de cette apparente disparité. Par exemple, pourquoi existe-t-il un Cdecl dans C # et __stdcall dans C ++? Apparemment, la stack est effacée deux fois, mais, dans les deux cas, les variables sont placées dans la stack dans le même ordre inverse, de sorte que je ne vois aucune erreur, même si les informations de retour sont effacées en cas de tenter une trace lors du débogage?

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

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, de plus, pourquoi y a- __stdcall il __stdcall en C ++?

Merci d’avance!

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

    Je vais essayer d’annoter cette histoire et leur pertinence au-delà de ce qui est dans l’article de Wikipedia . Le sharepoint départ est que les choix à faire 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 trouvé sa place dans la programmation Windows grâce à l’ancienne convention d’appel pascal 16 bits, utilisée dans Windows 16 bits et OS / 2. C’est la convention utilisée par toutes les fonctions Windows API ainsi que par COM. Comme la plupart des pinvoke étaient destinés à faire des appels au système d’exploitation, Stdcall est la valeur par défaut si vous ne la spécifiez pas explicitement dans l’atsortingbut [DllImport]. Sa seule et unique raison d’être est de spécifier que l’appelé nettoie. Ce qui produit un code plus compact, très important à l’époque où il fallait installer un système d’exploitation sur 640 kilo-octets de RAM. Son plus grand inconvénient est que c’est dangereux . Une discordance entre ce que suppose l’appelant est les arguments pour une fonction et ce que l’appelé a implémenté entraîne un déséquilibre de la stack. Ce qui peut à son tour rendre extrêmement difficile le diagnostic des accidents.

    • __cdecl est la convention d’appel standard pour le code écrit en langage C. Sa principale raison d’être est qu’il prend en charge les appels de fonctions avec un nombre variable d’arguments. Commun en code C avec des fonctions comme printf () et scanf (). Avec l’effet secondaire que puisque l’appelant 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 bogue très courant.

    • __fastcall est une convention d’appel assez mal définie avec des choix incompatibles entre eux. C’était commun chez les compilateurs Borland, une société autrefois très influente dans la technologie des compilateurs jusqu’à leur désintégration. Aussi l’ancien employeur de nombreux employés de Microsoft, y compris Anders Hejlsberg de renommée C #. Il a été inventé pour rendre le passage des arguments moins coûteux en en transmettant certains à travers des registres CPU au lieu de la stack. 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é pour un object de classe est passé aux méthodes d’instance d’une classe. Un détail supplémentaire en C ++ au-delà de C. Bien qu’il soit simple à implémenter, le marshaller pinvoke .NET ne le prend pas en charge. Une des principales raisons pour lesquelles vous ne pouvez pas identifier le code C ++. La complication n’est pas la convention d’appel, c’est la valeur correcte du pointeur this . Ce qui peut être très compliqué en raison de la prise en charge de l’inheritance multiple par C ++. Seul un compilateur C ++ peut jamais déterminer exactement ce qui doit être passé. Et seul le même compilateur C ++ qui a généré le code pour la classe C ++, différents compilateurs ont fait des choix différents sur la façon d’implémenter MI et sur son optimisation.

    • __clrcall est la convention d’appel du code managé. C’est un mélange des autres, ce pointeur passant comme __thiscall, l’argument optimisé passant par __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é. Cela permet donc aux concepteurs de profiter des avantages de toutes ces conventions, mais sans le moindre problème. Un exemple de la manière dont le code géré peut restr compétitif avec le code natif malgré la surcharge liée à la sécurité du code.

    Vous mentionnez extern "C" , comprendre l’importance de cela est également important pour survivre à l’interopérabilité. Les compilateurs de langage décorent souvent les noms de la fonction exscope avec des caractères supplémentaires. Aussi appelé “nom mangling”. C’est un truc plutôt merdique qui ne cesse jamais de causer des problèmes. Et vous devez le comprendre pour déterminer les valeurs correctes des propriétés CharSet, EntryPoint et ExactSpelling d’un atsortingbut [DllImport]. Il y a beaucoup de conventions:

    • Décoration api Windows. Windows était à l’origine un système d’exploitation non-Unicode, utilisant un codage sur 8 bits pour les chaînes. Windows NT était le premier à devenir Unicode à sa base. Cela a causé un problème de compatibilité assez important, l’ancien code n’aurait pas pu être exécuté sur les nouveaux systèmes d’exploitation car il transmettrait des chaînes codées sur 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 de 8 bits, un autre prend des chaînes Unicode. Et distingués 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 de Ansi en Unicode est inutile et sans perte.

    • La décoration standard des fonctions __stdcall est _foo @ 4. Un trait de soulignement principal et un suffixe @n indiquant la taille combinée des arguments. Ce postfixe a été conçu pour aider à résoudre le problème du déséquilibre de la stack si l’appelant et l’appelé ne sont pas d’accord sur le nombre d’arguments. Fonctionne bien, bien que le message d’erreur ne soit pas génial, le marshaller pinvoke vous dira qu’il ne peut pas trouver le point d’entrée. Notable est que Windows, tout en utilisant __stdcall, n’utilise pas cette décoration. C’était intentionnel, ce qui donnait aux programmeurs une chance d’obtenir l’argument GetProcAddress () correct. Le marshaller pinvoke s’en charge automatiquement, en essayant d’abord de trouver le point d’entrée avec le postfixe @, puis d’essayer celui sans.

    • La décoration standard de la fonction __cdecl est _foo. Un seul trait de soulignement. Le marshaller Pinvoke le sortinge automatiquement. Malheureusement, le postfix @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 la gestion des noms, 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 utilisant des outils de langage C hérités pour créer le programme. Ce qui a rendu nécessaire de distinguer, par exemple, un void foo(char) et une surcharge 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 de gabarit 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 déceler les inadéquations. 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 SDK undname.exe est très pratique pour convertir un nom déformé à sa déclaration C ++ d’origine.

    Donc, cela devrait effacer les propriétés. Vous utilisez EntryPoint pour donner le nom exact de la fonction exscope, qui pourrait ne pas correspondre à ce que vous voulez appeler dans votre propre code, en particulier pour les noms en 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 nourrir ma crampe pendant un moment maintenant. La réponse à votre question devrait être claire, Stdcall est la valeur par défaut mais ne correspond pas au code écrit en C ou C ++. Et votre déclaration [DllImport] n’est pas compatible. Cela devrait générer 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 mauvaises déclarations. Et peut plutôt planter de manière aléatoire votre code, en particulier dans la version Release. Assurez-vous de ne pas désactiver le MDA.

    cdecl et stdcall sont tous deux valides et utilisables entre C ++ et .NET, mais ils doivent être cohérents entre les deux mondes non gérés et gérés. Donc, votre déclaration C # pour InvokedFunction n’est 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.