Pourquoi avons-nous besoin d'extern "C" {#include <foo.h>} en C ++?


136

Pourquoi devons-nous utiliser:

extern "C" {
#include <foo.h>
}

Plus précisément:

  • Quand devons-nous l'utiliser?

  • Que se passe-t-il au niveau du compilateur / éditeur de liens qui nous oblige à l'utiliser?

  • Comment en termes de compilation / liaison cela résout-il les problèmes qui nous obligent à l'utiliser?

Réponses:


122

C et C ++ sont superficiellement similaires, mais chacun se compile dans un ensemble de code très différent. Lorsque vous incluez un fichier d'en-tête avec un compilateur C ++, le compilateur attend du code C ++. Si, cependant, il s'agit d'un en-tête C, alors le compilateur s'attend à ce que les données contenues dans le fichier d'en-tête soient compilées dans un certain format - le C ++ 'ABI' ou 'Application Binary Interface', de sorte que l'éditeur de liens s'étouffe. Cela est préférable à la transmission de données C ++ à une fonction qui attend des données C.

(Pour entrer dans le vif du sujet, l'ABI de C ++ `` déforme '' généralement les noms de leurs fonctions / méthodes, donc en appelant printf()sans marquer le prototype comme une fonction C, le C ++ générera en fait un appel de code _Zprintf, plus de la merde supplémentaire à la fin. )

Donc: à utiliser extern "C" {...}lorsque vous incluez un en-tête AC - c'est aussi simple que cela. Sinon, vous aurez une incompatibilité dans le code compilé et l'éditeur de liens s'étouffera. Pour la plupart des en-têtes, cependant, vous n'en aurez même pas besoin externcar la plupart des en-têtes système C prendront déjà en compte le fait qu'ils pourraient être inclus par le code C ++ et déjà par externleur code.


1
Pourriez-vous s'il vous plaît élaborer plus sur «la plupart des en-têtes système C prendront déjà en compte le fait qu'ils pourraient être inclus par le code C ++ et déjà externes à leur code». ?
Bulat M.

7
@BulatM. Ils contiennent quelque chose comme ceci: #ifdef __cplusplus extern "C" { #endif Ainsi, lorsqu'ils sont inclus à partir d'un fichier C ++, ils sont toujours traités comme un en-tête C.
Calmarius

111

extern "C" détermine comment les symboles du fichier objet généré doivent être nommés. Si une fonction est déclarée sans extern "C", le nom du symbole dans le fichier objet utilisera le changement de nom C ++. Voici un exemple.

Compte tenu du test.C comme ceci:

void foo() { }

La compilation et la liste des symboles dans le fichier objet donne:

$ g++ -c test.C
$ nm test.o
0000000000000000 T _Z3foov
                 U __gxx_personality_v0

La fonction foo est en fait appelée "_Z3foov". Cette chaîne contient des informations de type pour le type de retour et les paramètres, entre autres. Si vous écrivez à la place test.C comme ceci:

extern "C" {
    void foo() { }
}

Ensuite, compilez et regardez les symboles:

$ g++ -c test.C
$ nm test.o
                 U __gxx_personality_v0
0000000000000000 T foo

Vous obtenez un lien C. Le nom de la fonction "foo" dans le fichier objet est juste "foo", et il n'a pas toutes les informations de type fantaisie qui proviennent de la transformation des noms.

Vous incluez généralement un en-tête dans extern "C" {} si le code qui l'accompagne a été compilé avec un compilateur C mais que vous essayez de l'appeler depuis C ++. Lorsque vous faites cela, vous dites au compilateur que toutes les déclarations de l'en-tête utiliseront le lien C. Lorsque vous liez votre code, vos fichiers .o contiendront des références à «foo», pas à «_Z3fooblah», ce qui, espérons-le, correspond à tout ce qui se trouve dans la bibliothèque que vous liez.

La plupart des bibliothèques modernes mettront des gardes autour de ces en-têtes afin que les symboles soient déclarés avec le bon lien. par exemple, dans de nombreux en-têtes standard, vous trouverez:

#ifdef __cplusplus
extern "C" {
#endif

... declarations ...

#ifdef __cplusplus
}
#endif

Cela garantit que lorsque le code C ++ inclut l'en-tête, les symboles de votre fichier objet correspondent à ce qui se trouve dans la bibliothèque C. Vous ne devriez avoir à mettre extern "C" {} autour de votre en-tête C que s'il est ancien et n'a pas déjà ces gardes.


22

En C ++, vous pouvez avoir différentes entités qui partagent un nom. Par exemple, voici une liste de fonctions toutes nommées foo :

  • A::foo()
  • B::foo()
  • C::foo(int)
  • C::foo(std::string)

Afin de les différencier tous, le compilateur C ++ créera des noms uniques pour chacun dans un processus appelé dénomination ou décoration. Les compilateurs C ne font pas cela. En outre, chaque compilateur C ++ peut le faire d'une manière différente.

extern "C" indique au compilateur C ++ de ne pas modifier le nom du code entre accolades. Cela vous permet d'appeler des fonctions C à partir de C ++.


14

Cela a à voir avec la façon dont les différents compilateurs effectuent le changement de nom. Un compilateur C ++ déformera le nom d'un symbole exporté à partir du fichier d'en-tête d'une manière complètement différente de celle d'un compilateur C, donc lorsque vous essayez de lier, vous obtiendrez une erreur de l'éditeur de liens indiquant qu'il manquait des symboles.

Pour résoudre ce problème, nous demandons au compilateur C ++ de s'exécuter en mode "C", de sorte qu'il effectue la modification des noms de la même manière que le compilateur C. Cela fait, les erreurs de l'éditeur de liens sont corrigées.


11

C et C ++ ont des règles différentes sur les noms des symboles. Les symboles indiquent comment l'éditeur de liens sait que l'appel à la fonction "openBankAccount" dans un fichier objet produit par le compilateur est une référence à cette fonction que vous avez appelée "openBankAccount" dans un autre fichier objet produit à partir d'un fichier source différent par le même (ou compatible) compilateur. Cela vous permet de créer un programme à partir de plusieurs fichiers source, ce qui est un soulagement lorsque vous travaillez sur un grand projet.

En C, la règle est très simple, les symboles sont tous dans un seul espace de nom de toute façon. Ainsi, l'entier "chaussettes" est stocké en tant que "chaussettes" et la fonction count_socks est stockée en tant que "count_socks".

Les liens ont été construits pour C et d'autres langages comme C avec cette règle de dénomination des symboles simple. Les symboles dans l'éditeur de liens ne sont donc que de simples chaînes.

Mais en C ++, le langage vous permet d'avoir des espaces de noms, un polymorphisme et diverses autres choses qui entrent en conflit avec une règle aussi simple. Les six fonctions polymorphes appelées "add" doivent avoir des symboles différents, sinon le mauvais sera utilisé par d'autres fichiers objets. Cela se fait en "mutilant" (c'est un terme technique) les noms des symboles.

Lors de la liaison de code C ++ à des bibliothèques ou à du code C, vous avez besoin de tout élément externe «C» écrit en C, tel que des fichiers d'en-tête pour les bibliothèques C, pour indiquer à votre compilateur C ++ que ces noms de symboles ne doivent pas être mutilés, alors que le reste de votre code C ++ doit bien sûr être mutilé ou il ne fonctionnera pas.


11

Quand devons-nous l'utiliser?

Lorsque vous liez des bibliothèques C dans des fichiers objets C ++

Que se passe-t-il au niveau du compilateur / éditeur de liens qui nous oblige à l'utiliser?

C et C ++ utilisent des schémas différents pour la dénomination des symboles. Cela indique à l'éditeur de liens d'utiliser le schéma de C lors de la liaison dans la bibliothèque donnée.

Comment en termes de compilation / liaison cela résout-il les problèmes qui nous obligent à l'utiliser?

L'utilisation du schéma de dénomination C vous permet de référencer des symboles de style C. Sinon, l'éditeur de liens essaierait des symboles de style C ++ qui ne fonctionneraient pas.


7

Vous devez utiliser extern "C" chaque fois que vous incluez un en-tête définissant des fonctions résidant dans un fichier compilé par un compilateur C, utilisé dans un fichier C ++. (De nombreuses bibliothèques C standard peuvent inclure cette vérification dans leurs en-têtes pour le rendre plus simple pour le développeur)

Par exemple, si vous avez un projet avec 3 fichiers, util.c, util.h et main.cpp et que les fichiers .c et .cpp sont compilés avec le compilateur C ++ (g ++, cc, etc.) alors ce n'est pas t vraiment nécessaire, et peut même provoquer des erreurs de l'éditeur de liens. Si votre processus de construction utilise un compilateur C normal pour util.c, vous devrez utiliser extern "C" lorsque vous incluez util.h.

Ce qui se passe, c'est que C ++ encode les paramètres de la fonction dans son nom. C'est ainsi que fonctionne la surcharge de fonctions. Tout ce qui a tendance à arriver à une fonction C est l'ajout d'un trait de soulignement ("_") au début du nom. Sans utiliser extern "C", l'éditeur de liens recherchera une fonction nommée DoSomething @@ int @ float () lorsque le nom réel de la fonction est _DoSomething () ou juste DoSomething ().

L'utilisation d'extern "C" résout le problème ci-dessus en indiquant au compilateur C ++ qu'il doit rechercher une fonction qui suit la convention de dénomination C au lieu de celle C ++.


7

Le compilateur C ++ crée des noms de symboles différemment du compilateur C. Donc, si vous essayez de faire un appel à une fonction qui réside dans un fichier C, compilé en tant que code C, vous devez indiquer au compilateur C ++ que les noms de symboles qu'il tente de résoudre sont différents de ceux par défaut; sinon, l'étape de liaison échouera.


6

La extern "C" {}construction indique au compilateur de ne pas modifier les noms déclarés entre accolades. Normalement, le compilateur C ++ "améliore" les noms de fonction afin qu'ils codent les informations de type sur les arguments et la valeur de retour; c'est ce qu'on appelle le nom mutilé . La extern "C"construction empêche la mutilation.

Il est généralement utilisé lorsque le code C ++ doit appeler une bibliothèque en langage C. Il peut également être utilisé lors de l'exposition d'une fonction C ++ (à partir d'une DLL, par exemple) à des clients C.


5

Ceci est utilisé pour résoudre les problèmes de modification des noms. extern C signifie que les fonctions sont dans une API de type C "plate".


0

Décompilez un g++binaire généré pour voir ce qui se passe

Pour comprendre pourquoi externest nécessaire, la meilleure chose à faire est de comprendre ce qui se passe en détail dans les fichiers objets avec un exemple:

main.cpp

void f() {}
void g();

extern "C" {
    void ef() {}
    void eg();
}

/* Prevent g and eg from being optimized away. */
void h() { g(); eg(); }

Compilez avec la sortie ELF Linux GCC 4.8 :

g++ -c main.cpp

Décompilez la table des symboles:

readelf -s main.o

La sortie contient:

Num:    Value          Size Type    Bind   Vis      Ndx Name
  8: 0000000000000000     6 FUNC    GLOBAL DEFAULT    1 _Z1fv
  9: 0000000000000006     6 FUNC    GLOBAL DEFAULT    1 ef
 10: 000000000000000c    16 FUNC    GLOBAL DEFAULT    1 _Z1hv
 11: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND _Z1gv
 12: 0000000000000000     0 NOTYPE  GLOBAL DEFAULT  UND eg

Interprétation

On voit ça:

  • efet egont été stockés dans des symboles avec le même nom que dans le code

  • les autres symboles étaient mutilés. Démêlons-les:

    $ c++filt _Z1fv
    f()
    $ c++filt _Z1hv
    h()
    $ c++filt _Z1gv
    g()

Conclusion: les deux types de symboles suivants n'ont pas été mutilés:

  • défini
  • déclaré mais non défini ( Ndx = UND), à fournir au moment du lien ou de l'exécution à partir d'un autre fichier objet

Vous aurez donc besoin des extern "C"deux lors de l'appel:

  • C à partir de C ++: dites g++d'attendre des symboles démêlés produits pargcc
  • C ++ à partir de C: dire g++de générer des symboles démêlés gccà utiliser

Choses qui ne fonctionnent pas dans extern C

Il devient évident que toute fonctionnalité C ++ nécessitant une modification des noms ne fonctionnera pas à l'intérieur extern C:

extern "C" {
    // Overloading.
    // error: declaration of C function ‘void f(int)’ conflicts with
    void f();
    void f(int i);

    // Templates.
    // error: template with C linkage
    template <class C> void f(C i) { }
}

Exemple de C exécutable minimal à partir de C ++

Par souci d'exhaustivité et pour les nouveautés, voir aussi: Comment utiliser des fichiers source C dans un projet C ++?

L'appel de C à partir de C ++ est assez simple: chaque fonction C n'a qu'un seul symbole non mutilé possible, donc aucun travail supplémentaire n'est requis.

main.cpp

#include <cassert>

#include "c.h"

int main() {
    assert(f() == 1);
}

ch

#ifndef C_H
#define C_H

/* This ifdef allows the header to be used from both C and C++. */
#ifdef __cplusplus
extern "C" {
#endif
int f();
#ifdef __cplusplus
}
#endif

#endif

cc

#include "c.h"

int f(void) { return 1; }

Courir:

g++ -c -o main.o -std=c++98 main.cpp
gcc -c -o c.o -std=c89 c.c
g++ -o main.out main.o c.o
./main.out

Sans extern "C"le lien échoue avec:

main.cpp:6: undefined reference to `f()'

car g++s'attend à trouver un mutilé f, qui gccn'a pas produit.

Exemple sur GitHub .

Exemple C ++ exécutable minimal à partir de C

Appeler C ++ depuis est un peu plus difficile: nous devons créer manuellement des versions non mutilées de chaque fonction que nous voulons exposer.

Ici, nous illustrons comment exposer les surcharges de fonctions C ++ à C.

principal c

#include <assert.h>

#include "cpp.h"

int main(void) {
    assert(f_int(1) == 2);
    assert(f_float(1.0) == 3);
    return 0;
}

cpp.h

#ifndef CPP_H
#define CPP_H

#ifdef __cplusplus
// C cannot see these overloaded prototypes, or else it would get confused.
int f(int i);
int f(float i);
extern "C" {
#endif
int f_int(int i);
int f_float(float i);
#ifdef __cplusplus
}
#endif

#endif

cpp.cpp

#include "cpp.h"

int f(int i) {
    return i + 1;
}

int f(float i) {
    return i + 2;
}

int f_int(int i) {
    return f(i);
}

int f_float(float i) {
    return f(i);
}

Courir:

gcc -c -o main.o -std=c89 -Wextra main.c
g++ -c -o cpp.o -std=c++98 cpp.cpp
g++ -o main.out main.o cpp.o
./main.out

Sans extern "C"cela échoue avec:

main.c:6: undefined reference to `f_int'
main.c:7: undefined reference to `f_float'

parce que g++généré des symboles mutilés qui gccne peuvent pas trouver.

Exemple sur GitHub .

Testé dans Ubuntu 18.04.


1
Merci d'avoir expliqué le vote défavorable, tout a du sens maintenant.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.