Pourquoi les fonctions C ne peuvent-elles pas être dénommées?

136

J'ai eu une interview récemment et une question posée était de savoir à quoi sert le extern "C"code C ++. J'ai répondu qu'il s'agissait d'utiliser les fonctions C dans le code C ++ car C n'utilise pas la manipulation de noms. On m'a demandé pourquoi C n'utilisait pas de dénigrement et pour être honnête, je ne pouvais pas répondre.

Je comprends que lorsque le compilateur C ++ compile des fonctions, il donne un nom spécial à la fonction principalement parce que nous pouvons avoir des fonctions surchargées du même nom en C ++ qui doivent être résolues au moment de la compilation. En C, le nom de la fonction restera le même, ou peut-être précédé d'un _.

Ma question est la suivante: qu'est-ce qui ne va pas avec le fait d'autoriser le compilateur C ++ à modifier également les fonctions C? J'aurais supposé que peu importe les noms que le compilateur leur donne. Nous appelons les fonctions de la même manière en C et C ++.

Ingénieur999
la source
75
C n'a pas besoin de modifier les noms, car il n'a pas de surcharge de fonctions.
EOF
9
Comment liez-vous des bibliothèques C avec du code C ++ si le compilateur C ++ manie les noms des fonctions?
Mat
6
"J'ai répondu que c'était pour utiliser les fonctions C dans le code C ++ car C n'utilise pas la gestion des noms." - Je pense que c'est l'inverse. L'externe "C" rend les fonctions C ++ utilisables dans un compilateur C. source
rozina
3
@ Engineer999: Et si vous compilez le sous-ensemble de C qui est aussi C ++ avec un compilateur C ++, les noms des fonctions seront en effet mutilés. Mais si vous voulez pouvoir lier des binaires créés avec différents compilateurs, vous ne voulez pas de changement de nom.
EOF
13
C modifie les noms. En général, le nom mutilé est le nom de la fonction précédé d'un trait de soulignement. Parfois, c'est le nom de la fonction suivi d'un trait de soulignement. extern "C"dit de modifier le nom de la même manière que "le" compilateur C le ferait.
Pete Becker

Réponses:

187

Cela a été en quelque sorte répondu ci-dessus, mais je vais essayer de mettre les choses en contexte.

Tout d'abord, C est venu en premier. En tant que tel, ce que fait C est en quelque sorte la "valeur par défaut". Il ne modifie pas les noms parce que ce n'est tout simplement pas le cas. Un nom de fonction est un nom de fonction. Un global est un global, et ainsi de suite.

Puis le C ++ est arrivé. C ++ voulait pouvoir utiliser le même éditeur de liens que C, et pouvoir établir des liens avec du code écrit en C. Mais C ++ ne pouvait pas laisser le C "mangling" (ou, il en manque) tel quel. Consultez l'exemple suivant:

int function(int a);
int function();

En C ++, ce sont des fonctions distinctes, avec des corps distincts. Si aucun d'entre eux n'est mutilé, les deux seront appelés "fonction" (ou "_function"), et l'éditeur de liens se plaindra de la redéfinition d'un symbole. La solution C ++ a consisté à transformer les types d'arguments dans le nom de la fonction. Ainsi, l'un est appelé _function_intet l'autre est appelé _function_void(pas de schéma de mutilation réel) et la collision est évitée.

Maintenant, il nous reste un problème. Si a int function(int a)été défini dans un module C, et que nous prenons simplement son en-tête (c'est-à-dire la déclaration) dans du code C ++ et l'utilisons, le compilateur générera une instruction à l'éditeur de liens à importer _function_int. Lorsque la fonction a été définie, dans le module C, elle ne s'appelait pas ainsi. Ça s'appelait_function . Cela provoquera une erreur de l'éditeur de liens.

Pour éviter cette erreur, pendant la déclaration de la fonction, nous disons au compilateur qu'il s'agit d'une fonction conçue pour être liée ou compilée par un compilateur C:

extern "C" int function(int a);

Le compilateur C ++ sait maintenant importer _functionplutôt que _function_int, et tout va bien.

Shachar Shemesh
la source
1
@ShacharShamesh: J'ai posé cette question ailleurs, mais qu'en est-il des liens dans les bibliothèques compilées C ++? Lorsque le compilateur parcourt et compile mon code qui appelle l'une des fonctions d'une bibliothèque compilée C ++, comment sait-il quel nom modifier ou donner à la fonction en voyant simplement sa déclaration ou son appel de fonction? Comment savoir que là où il est défini, il se transforme en autre chose? Il doit donc y avoir une méthode standard de gestion des noms en C ++?
Engineer999
2
Chaque compilateur le fait à sa manière. Si vous compilez tout avec le même compilateur, cela n'a pas d'importance. Mais si vous essayez d'utiliser, par exemple, une bibliothèque qui a été compilée avec le compilateur de Borland, à partir d'un programme que vous construisez avec le compilateur de Microsoft, eh bien ... bonne chance; vous en aurez besoin :)
Mark VY
6
@ Engineer999 Vous êtes-vous déjà demandé pourquoi les bibliothèques C ++ portables n'existent pas, mais elles spécifient exactement la version (et les indicateurs) du compilateur (et la bibliothèque standard) que vous devez utiliser ou simplement exporter une API C? Voilà. C ++ est à peu près le langage le moins portable jamais inventé, tandis que C est exactement le contraire. Il y a des efforts à cet égard, mais pour l'instant, si vous voulez quelque chose de vraiment portable, vous vous en tiendrez à C.
Voo
1
@Voo Eh bien, en théorie, vous devriez être capable d'écrire du code portable simplement en adhérant à la norme, par exemple -std=c++11, et en évitant d'utiliser quoi que ce soit en dehors de la norme. C'est la même chose que de déclarer une version Java (bien que les versions plus récentes de Java soient rétrocompatibles). Ce n'est pas la faute aux normes que les gens utilisent des extensions spécifiques au compilateur et du code dépendant de la plateforme. D'un autre côté, vous ne pouvez pas les blâmer, car il manque beaucoup de choses (en particulier IO, comme les sockets) dans la norme. Le comité semble rattraper lentement cela. Corrigez-moi si j'ai raté quelque chose.
mucaho
14
@mucaho: vous parlez de portabilité / compatibilité des sources. c'est-à-dire l'API. Voo parle de compatibilité binaire , sans recompilation. Cela nécessite une compatibilité ABI . Les compilateurs C ++ changent régulièrement leur ABI entre les versions. (par exemple, g ++ n'essaye même pas d'avoir un ABI stable. Je suppose qu'ils ne cassent pas l'ABI juste pour le plaisir, mais ils n'évitent pas les changements qui nécessitent un changement d'ABI quand il y a quelque chose à gagner et pas d'autre bon moyen pour le faire.).
Peter Cordes
45

Ce n'est pas qu'ils "ne peuvent pas", ils ne le sont pas , en général.

Si vous voulez appeler une fonction dans une bibliothèque C appelée foo(int x, const char *y), il ne sert à rien de laisser votre compilateur C ++ le transformer enfoo_I_cCP() (ou autre chose, juste inventé un schéma de transformation sur place ici) simplement parce qu'il le peut.

Ce nom ne résoudra pas, la fonction est en C et son nom ne dépend pas de sa liste de types d'arguments. Ainsi, le compilateur C ++ doit le savoir et marquer cette fonction comme étant C pour éviter de faire le mangling.

N'oubliez pas que ladite fonction C peut être dans une bibliothèque dont vous n'avez pas le code source, tout ce que vous avez est le binaire précompilé et l'en-tête. Donc, votre compilateur C ++ ne peut pas faire "c'est propre", il ne peut pas changer ce qui est dans la bibliothèque après tout.

se détendre
la source
C'est la partie qui me manque. Pourquoi le compilateur C ++ mangle-t-il un nom de fonction quand il voit juste sa déclaration ou la voit être appelée? Ne modifie-t-il pas simplement les noms de fonctions lorsqu'il voit leur implémentation? Cela aurait plus de sens pour moi
Engineer999
13
@ Engineer999: Comment pouvez-vous avoir un nom pour la définition et un autre pour la déclaration? "Il y a une fonction appelée Brian que vous pouvez appeler." "D'accord, je vais appeler Brian." "Désolé, il n'y a pas de fonction appelée Brian." Il s'avère que ça s'appelle Graham.
Courses de légèreté en orbite le
Qu'en est-il des liens dans les bibliothèques compilées C ++? Lorsque le compilateur parcourt et compile notre code qui appelle l'une des fonctions d'une bibliothèque compilée C ++, comment sait-il quel nom modifier ou donner à la fonction en voyant simplement sa déclaration ou son appel de fonction?
Engineer999
1
@ Engineer999 Les deux doivent s'entendre sur la même mutilation. Alors ils voient le fichier d'en-tête (rappelez-vous, il y a très peu de métadonnées dans les DLL natives - les en-têtes sont ces métadonnées), et disent "Ah, c'est vrai, Brian devrait vraiment être Graham". Si cela ne fonctionne pas (par exemple avec deux schémas de mutilation incompatibles), vous n'obtiendrez pas un lien correct et votre application échouera. C ++ a beaucoup d'incompatibilités comme celle-ci. Dans la pratique, vous devez alors utiliser explicitement le nom mutilé et désactiver la manipulation de votre côté (par exemple, vous dites à votre code d'exécuter Graham, pas Brian). Dans la pratique réelle ... extern "C":)
Luaan
1
@ Engineer999 Je me trompe peut-être, mais avez-vous peut-être de l'expérience avec des langages comme Visual Basic, C # ou Java (ou même Pascal / Delphi dans une certaine mesure)? Celles-ci font que l'interopérabilité semble extrêmement simple. En C et en particulier en C ++, c'est tout sauf. Il y a beaucoup de conventions d'appel que vous devez respecter, vous devez savoir qui est responsable de quelle mémoire et vous devez avoir les fichiers d'en-tête qui vous indiquent les déclarations de fonction, car les DLL elles-mêmes ne contiennent pas suffisamment d'informations - en particulier dans le cas de pure C. Si vous n'avez pas de fichier d'en-tête, vous devez généralement décompiler la DLL pour l'utiliser.
Luaan
32

qu'est-ce qui ne va pas avec le fait d'autoriser le compilateur C ++ à modifier également les fonctions C?

Ce ne seraient plus des fonctions C.

Une fonction n'est pas seulement une signature et une définition; le fonctionnement d'une fonction est largement déterminé par des facteurs tels que la convention d'appel. L '"Interface binaire d'application" spécifiée pour une utilisation sur votre plate-forme décrit comment les systèmes se parlent. L'ABI C ++ utilisé par votre système spécifie un schéma de modification des noms, de sorte que les programmes de ce système sachent comment appeler des fonctions dans des bibliothèques, etc.(Lisez l'ABI Itanium C ++ pour un excellent exemple. Vous verrez très vite pourquoi c'est nécessaire.)

Il en va de même pour le C ABI de votre système. Certaines ABI C ont en fait un schéma de transformation des noms (par exemple Visual Studio), donc il s'agit moins de "désactiver la manipulation de noms" et plus de passer de l'ABI C ++ à l'ABI C, pour certaines fonctions. Nous marquons les fonctions C comme étant des fonctions C, pour lesquelles l'ABI C (plutôt que l'ABI C ++) est pertinent. La déclaration doit correspondre à la définition (que ce soit dans le même projet ou dans une bibliothèque tierce), sinon la déclaration est inutile. Sans cela, votre système ne saura tout simplement pas comment localiser / invoquer ces fonctions.

Quant à savoir pourquoi les plates-formes ne définissent pas les ABI C et C ++ comme étant les mêmes et se débarrassent de ce «problème», c'est en partie historique - les ABI C d'origine n'étaient pas suffisants pour C ++, qui a des espaces de noms, des classes et une surcharge d'opérateurs, tout dont doivent d'une manière ou d'une autre être représentés dans le nom d'un symbole d'une manière conviviale pour l'ordinateur - mais on pourrait également affirmer que faire en sorte que les programmes C respectent maintenant le C ++ est injuste pour la communauté C, qui devrait supporter une situation beaucoup plus compliquée ABI juste pour le bien d'autres personnes qui veulent l'interopérabilité.

Courses de légèreté en orbite
la source
2
+int(PI/3), mais avec un grain de sel: je serais très prudent de parler de "C ++ ABI" ... AFAIK, il y a des tentatives pour définir des ABI C ++, mais pas de véritables standards de facto / de jure - comme isocpp.org/files /papers/n4028.pdf déclare (et je suis tout à fait d'accord), citation, il est profondément ironique que C ++ ait toujours soutenu un moyen de publier une API avec un ABI binaire stable - en recourant au sous-ensemble C de C ++ via extern «C ». . C++ Itanium ABIest juste ça - un ABI C ++ pour Itanium ... comme discuté sur stackoverflow.com/questions/7492180/c-abi-issues-list
3
@vaxquis: Ouais, pas "ABI C ++", mais "un ABI C ++" de la même manière que j'ai une "clé maison" qui ne fonctionne pas sur toutes les maisons. Je suppose que cela pourrait être plus clair, même si j'ai essayé de le rendre aussi clair que possible en commençant par la phrase «L'ABI C ++ utilisé par votre système » . J'ai laissé tomber le clarificateur dans les énoncés ultérieurs par souci de concision, mais j'accepterai une modification qui réduit la confusion ici!
Courses de légèreté en orbite le
1
AIUI C abi avait tendance à être une propriété d'une plate-forme tandis que les ABI C ++ avaient tendance à être une propriété d'un compilateur individuel et souvent même une propriété d'une version individuelle d'un compilateur. Donc, si vous vouliez créer un lien entre des modules construits avec des outils de différents fournisseurs, vous deviez utiliser un C abi pour l'interface.
plugwash
La déclaration "les fonctions mutilées par nom ne seraient plus des fonctions C" est exagérée - il est parfaitement possible d'appeler des fonctions mutilées par nom à partir du C pur et simple si le nom mutilé est connu. Que le nom change ne le rend pas moins adhérent à l'ABI C, c'est-à-dire n'en fait pas moins une fonction C. L'inverse est plus logique - le code C ++ ne pourrait pas appeler une fonction C sans la déclarer «C» car cela ferait un mal de nom lors d'une tentative de liaison avec l'appelé.
Peter - Réintègre Monica le
@ PeterA.Schneider: Oui, la phrase du titre est exagérée. Le reste de la réponse contient les détails factuels pertinents.
Courses de légèreté en orbite le
21

MSVC modifie en fait les noms C, bien que de manière simple. Il ajoute parfois @4ou un autre petit nombre. Cela concerne les conventions d'appel et la nécessité de nettoyer la pile.

Donc, la prémisse est tout simplement imparfaite.

MSalters
la source
2
Ce n'est pas vraiment du mal de nom. Il s'agit simplement d'une convention de dénomination (ou ornant le nom) spécifique au fournisseur pour éviter les problèmes liés aux exécutables liés à des DLL construites avec des fonctions ayant des conventions d'appel différentes.
Peter
2
Qu'en est-il du préfixe avec un _?
OrangeDog
12
@Peter: Littéralement la même chose.
Courses de légèreté en orbite le
5
@Frankie_C: "L'appelant nettoie la pile" n'est spécifié par aucun standard C: aucune des conventions d'appel n'est plus standard que l'autre du point de vue du langage.
Ben Voigt le
2
Et du point de vue MSVC, la "convention d'appel standard" est exactement ce que vous choisissez /Gd, /Gr, /Gv, /Gz. (C'est-à-dire que la convention d'appel standard est ce qui est utilisé à moins qu'une déclaration de fonction spécifie explicitement une convention d'appel.). Vous pensez à __cdecllaquelle est la convention d'appel standard par défaut.
MSalters le
13

Il est très courant d'avoir des programmes qui sont partiellement écrits en C et partiellement écrits dans un autre langage (souvent en langage d'assemblage, mais parfois en Pascal, FORTRAN ou autre). Il est également courant que les programmes contiennent différents composants écrits par différentes personnes qui ne disposent pas du code source pour tout.

Sur la plupart des plates-formes, il existe une spécification - souvent appelée ABI [Application Binary Interface] qui décrit ce qu'un compilateur doit faire pour produire une fonction avec un nom particulier qui accepte des arguments de certains types particuliers et renvoie une valeur d'un type particulier. Dans certains cas, une ABI peut définir plus d'une "convention d'appel"; les compilateurs pour de tels systèmes fournissent souvent un moyen d'indiquer quelle convention d'appel doit être utilisée pour une fonction particulière. Par exemple, sur Macintosh, la plupart des routines Toolbox utilisent la convention d'appel Pascal, donc le prototype de quelque chose comme "LineTo" serait quelque chose comme:

/* Note that there are no underscores before the "pascal" keyword because
   the Toolbox was written in the early 1980s, before the Standard and its
   underscore convention were published */
pascal void LineTo(short x, short y);

Si tout le code d'un projet a été compilé à l'aide du même compilateur, peu importe le nom du compilateur exporté pour chaque fonction, mais dans de nombreuses situations, il sera nécessaire pour le code C d'appeler des fonctions qui ont été compilées à l'aide d'autres outils et ne peut pas être recompilé avec le compilateur actuel [et peut très bien ne pas être en C]. Être capable de définir le nom de l'éditeur de liens est donc essentiel à l'utilisation de telles fonctions.

supercat
la source
Oui, c'est la réponse. S'il ne s'agit que de C et C ++, il est difficile de comprendre pourquoi cela se fait de cette façon. Pour comprendre, nous devons replacer les choses dans le contexte de l'ancienne méthode de liaison statique. La liaison statique semble primitive aux programmeurs Windows, mais c'est la principale raison pour laquelle C ne peut pas modifier les noms.
user34660
2
@ user34660: Pas qutie. C'est la raison pour laquelle C ne peut pas imposer l'existence de fonctionnalités dont la mise en œuvre nécessiterait soit de modifier les noms exportables, soit d'autoriser l'existence de plusieurs symboles portant le même nom qui se distinguent par des caractéristiques secondaires.
supercat
Savons-nous qu'il y a eu des tentatives de "mandater" de telles choses ou que de telles choses étaient des extensions disponibles pour C avant C ++?
user34660
@ user34660: Re "La liaison statique semble primitive pour les programmeurs Windows ...", mais la liaison dynamique semble parfois être un PITA majeur pour les utilisateurs de Linux, lorsque l'installation du programme X (probablement écrit en C ++) signifie avoir à rechercher et installer des versions particulières des bibliothèques dont vous disposez déjà de différentes versions sur votre système.
jamesqf
@jamesqf, oui, Unix n'avait pas de liaison dynamique avant Windows. Je connais très peu la liaison dynamique sous Unix / Linux, mais il semble que ce n'est pas aussi transparent que cela pourrait l'être dans un système d'exploitation en général.
user34660
12

J'ajouterai une autre réponse, pour aborder certaines des discussions tangentielles qui ont eu lieu.

L'ABI C (interface binaire d'application) appelait à l'origine à passer des arguments sur la pile dans l'ordre inverse (c'est-à-dire poussé de droite à gauche), où l'appelant libère également le stockage de la pile. L'ABI moderne utilise en fait des registres pour passer des arguments, mais la plupart des considérations de déformation remontent à la transmission d'arguments de la pile d'origine.

L'ABI Pascal original, en revanche, poussait les arguments de gauche à droite, et l'appelé devait faire apparaître les arguments. L'ABI C original est supérieur à l'ABI Pascal original sur deux points importants. L'ordre d'envoi des arguments signifie que le décalage de pile du premier argument est toujours connu, autorisant les fonctions qui ont un nombre inconnu d'arguments, où les premiers arguments contrôlent le nombre d'autres arguments (ala printf).

La deuxième façon dont l'ABI C est supérieur est le comportement au cas où l'appelant et l'appelé ne s'accordent pas sur le nombre d'arguments. Dans le cas C, tant que vous n'accédez pas réellement aux arguments après le dernier, rien de mal ne se passe. Dans Pascal, le nombre incorrect d'arguments est sorti de la pile et la pile entière est corrompue.

L'ABI Windows 3.1 d'origine était basé sur Pascal. En tant que tel, il a utilisé l'ABI Pascal (arguments dans l'ordre de gauche à droite, l'appelé apparaît). Étant donné que toute non-concordance dans le numéro d'argument peut entraîner une corruption de la pile, un schéma de mutilation a été formé. Chaque nom de fonction a été mutilé avec un nombre indiquant la taille, en octets, de ses arguments. Donc, sur une machine 16 bits, la fonction suivante (syntaxe C):

int function(int a)

A été mutilé function@2, parce queint fait deux octets de large. Cela a été fait pour que si la déclaration et la définition ne correspondent pas, l'éditeur de liens ne parviendra pas à trouver la fonction plutôt que de corrompre la pile au moment de l'exécution. Inversement, si le programme est lié, vous pouvez être sûr que le nombre correct d'octets est extrait de la pile à la fin de l'appel.

Windows 32 bits et versions ultérieures utilisent le stdcall ABI à la place. Il est similaire à l'ABI Pascal, sauf que l'ordre de poussée est comme en C, de droite à gauche. Comme le Pascal ABI, le nom qui modifie la taille en octets des arguments dans le nom de la fonction pour éviter la corruption de la pile.

Contrairement aux revendications faites ailleurs ici, l'ABI C ne modifie pas les noms des fonctions, même sur Visual Studio. Inversement, les fonctions de manipulation décorées avec la stdcallspécification ABI ne sont pas uniques à VS. GCC prend également en charge cette ABI, même lors de la compilation pour Linux. Ceci est largement utilisé par Wine , qui utilise son propre chargeur pour permettre la liaison au moment de l'exécution des binaires compilés Linux aux DLL compilées Windows.

Shachar Shemesh
la source
9

Les compilateurs C ++ utilisent la gestion des noms afin de permettre des noms de symboles uniques pour les fonctions surchargées dont la signature serait autrement la même. Il encode également les types d'arguments, ce qui permet un polymorphisme au niveau des fonctions.

C ne l'exige pas car il ne permet pas de surcharger les fonctions.

Notez que le changement de nom est une des raisons (mais certainement pas la seule!) Pour laquelle on ne peut pas se fier à un 'C ++ ABI'.

dégriffé
la source
8

C ++ veut pouvoir interopérer avec le code C qui se lie à lui, ou avec lequel il est lié.

C attend des noms de fonction non mutilés par le nom.

Si C ++ le mutilait, il ne trouverait pas les fonctions non mutilées exportées à partir de C, ou C ne trouverait pas les fonctions C ++ exportées. L'éditeur de liens C doit obtenir le nom qu'il attend lui-même, car il ne sait pas qu'il vient ou va vers C ++.

Yakk - Adam Nevraumont
la source
3

La gestion des noms des fonctions et des variables C permettrait de vérifier leurs types au moment de la liaison. Actuellement, toutes les implémentations (?) C vous permettent de définir une variable dans un fichier et de l'appeler en tant que fonction dans un autre. Ou vous pouvez déclarer une fonction avec une mauvaise signature (par exemple void fopen(double), puis l'appeler.

J'ai proposé un schéma pour la liaison de type sûr des variables et des fonctions C par l'utilisation de la mutilation en 1991. Le schéma n'a jamais été adopté, car, comme d'autres l'ont noté ici, cela détruirait la rétrocompatibilité.

Diomidis Spinellis
la source
1
Vous voulez dire "permettre à leurs types d'être vérifiés au moment de la liaison ". Les types sont vérifiés au moment de la compilation, mais la liaison avec des noms non mélangés ne peut pas vérifier si les déclarations utilisées dans les différentes unités de compilation concordent. Et s'ils ne sont pas d'accord, c'est votre système de construction qui est fondamentalement défectueux et doit être corrigé.
cmaster - réintégrer monica le