¿Por qué necesitamos "C" externa {#include <foo.h>} en C ++?


136

¿Por qué necesitamos usar:

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

Específicamente:

  • ¿Cuándo deberíamos usarlo?

  • ¿Qué está sucediendo en el nivel del compilador / enlazador que requiere que lo usemos?

  • ¿Cómo en términos de compilación / vinculación resuelve esto los problemas que requieren que lo usemos?

Respuestas:


122

C y C ++ son superficialmente similares, pero cada uno se compila en un conjunto de código muy diferente. Cuando incluye un archivo de encabezado con un compilador de C ++, el compilador espera el código de C ++. Sin embargo, si se trata de un encabezado C, el compilador espera que los datos contenidos en el archivo del encabezado se compilen en un formato determinado: el 'ABI' o 'Interfaz binaria de la aplicación' de C ++, por lo que el enlazador se bloquea. Esto es preferible a pasar datos de C ++ a una función que espera datos de C.

(Para entrar en el meollo de la cuestión, el ABI de C ++ generalmente 'manipula' los nombres de sus funciones / métodos, por lo que al llamar printf()sin marcar el prototipo como una función de C, el C ++ generará llamadas de código _Zprintf, más basura al final. )

Entonces: utilícelo extern "C" {...}cuando incluya un encabezado de CA: así de simple. De lo contrario, tendrá una falta de coincidencia en el código compilado, y el enlazador se ahogará. Sin embargo, para la mayoría de los encabezados, ni siquiera necesitará externporque la mayoría de los encabezados C del sistema ya tendrán en cuenta el hecho de que podrían estar incluidos en el código C ++ y ya en externsu código.


1
¿Podría dar más detalles sobre "la mayoría de los encabezados C del sistema ya tendrán en cuenta el hecho de que podrían estar incluidos en el código C ++ y ya externarán su código". ?
Bulat M.

77
@BulatM. Contienen algo como esto: #ifdef __cplusplus extern "C" { #endif Entonces, cuando se incluyen desde un archivo C ++, todavía se tratan como un encabezado C.
Calmarius

111

extern "C" determina cómo se deben nombrar los símbolos en el archivo objeto generado. Si se declara una función sin "C" externa, el nombre del símbolo en el archivo de objeto utilizará el cambio de nombre de C ++. Aquí hay un ejemplo.

Prueba dada. C así:

void foo() { }

Compilar y enumerar símbolos en el archivo objeto proporciona:

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

La función foo en realidad se llama "_Z3foov". Esta cadena contiene información de tipo para el tipo de retorno y los parámetros, entre otras cosas. Si en cambio escribes test.C así:

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

Luego compila y mira los símbolos:

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

Obtienes el enlace C. El nombre de la función "foo" en el archivo de objeto es simplemente "foo", y no tiene toda la información de tipo elegante que proviene del cambio de nombre.

Generalmente, incluye un encabezado dentro de "C" {} externo si el código que lo acompaña fue compilado con un compilador de C pero está intentando llamarlo desde C ++. Cuando haces esto, le estás diciendo al compilador que todas las declaraciones en el encabezado usarán el enlace C. Cuando vincule su código, sus archivos .o contendrán referencias a "foo", no a "_Z3fooblah", que con suerte coincide con lo que esté en la biblioteca con la que se está vinculando.

La mayoría de las bibliotecas modernas colocarán protectores alrededor de dichos encabezados para que los símbolos se declaren con el enlace correcto. por ejemplo, en muchos de los encabezados estándar encontrarás:

#ifdef __cplusplus
extern "C" {
#endif

... declarations ...

#ifdef __cplusplus
}
#endif

Esto asegura que cuando el código C ++ incluya el encabezado, los símbolos en su archivo de objeto coincidan con lo que hay en la biblioteca C. Solo deberías poner "C" {} externo alrededor de tu encabezado C si es viejo y no tiene estos guardias ya.


22

En C ++, puede tener diferentes entidades que comparten un nombre. Por ejemplo, aquí hay una lista de funciones todas denominadas foo :

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

Para diferenciarlos a todos, el compilador de C ++ creará nombres únicos para cada uno en un proceso llamado decoración o cambio de nombres. Los compiladores de C no hacen esto. Además, cada compilador de C ++ puede hacer esto de una manera diferente.

extern "C" le dice al compilador de C ++ que no realice ningún cambio de nombre en el código entre llaves. Esto le permite llamar a funciones de C desde C ++.


14

Tiene que ver con la forma en que los diferentes compiladores realizan el cambio de nombre. Un compilador de C ++ destrozará el nombre de un símbolo exportado desde el archivo de encabezado de una manera completamente diferente a la de un compilador de C, por lo que cuando intente vincular, obtendrá un error de vinculador que indica que faltan símbolos.

Para resolver esto, le decimos al compilador de C ++ que se ejecute en modo "C", por lo que realiza el cambio de nombre de la misma manera que lo haría el compilador de C. Una vez hecho esto, los errores del enlazador son corregidos.


11

C y C ++ tienen reglas diferentes sobre los nombres de los símbolos. Los símbolos son cómo el vinculador sabe que la llamada a la función "openBankAccount" en un archivo objeto producido por el compilador es una referencia a esa función que usted llamó "openBankAccount" en otro archivo objeto producido desde un archivo fuente diferente por el mismo (o compatible) compilador. Esto le permite crear un programa con más de un archivo fuente, lo cual es un alivio cuando trabaja en un proyecto grande.

En C la regla es muy simple, los símbolos están todos en un solo espacio de nombre de todos modos. Por lo tanto, el entero "calcetines" se almacena como "calcetines" y la función count_socks se almacena como "count_socks".

Los enlazadores se crearon para C y otros lenguajes como C con esta simple regla de denominación de símbolos. Entonces, los símbolos en el enlazador son simples cadenas.

Pero en C ++, el lenguaje le permite tener espacios de nombres y polimorfismo y otras cosas que entran en conflicto con una regla tan simple. Las seis funciones polimórficas llamadas "agregar" deben tener símbolos diferentes, de lo contrario, otros archivos de objeto utilizarán la incorrecta. Esto se hace "destrozando" (es un término técnico) los nombres de los símbolos.

Al vincular el código de C ++ a las bibliotecas o el código de C, necesita una "C" externa escrita en C, como los archivos de encabezado para las bibliotecas de C ++, para decirle al compilador de C ++ que estos nombres de símbolos no deben ser maltratados, mientras que el resto de su código C ++, por supuesto, debe ser destruido o no funcionará.


11

¿Cuándo deberíamos usarlo?

Cuando vincula bibliotecas C en archivos de objetos C ++

¿Qué está sucediendo en el nivel del compilador / enlazador que requiere que lo usemos?

C y C ++ usan diferentes esquemas para nombrar símbolos. Esto le dice al vinculador que use el esquema de C cuando se vincula en la biblioteca dada.

¿Cómo en términos de compilación / vinculación resuelve esto los problemas que requieren que lo usemos?

El uso del esquema de nombres C le permite hacer referencia a símbolos de estilo C. De lo contrario, el enlazador probaría símbolos de estilo C ++ que no funcionarían.


7

Debe usar "C" externa siempre que incluya un encabezado que defina funciones que residen en un archivo compilado por un compilador de C, usado en un archivo de C ++. (Muchas bibliotecas C estándar pueden incluir esta verificación en sus encabezados para que sea más simple para el desarrollador)

Por ejemplo, si tiene un proyecto con 3 archivos, util.c, util.h y main.cpp y ambos archivos .c y .cpp se compilan con el compilador de C ++ (g ++, cc, etc.) entonces no es No es realmente necesario, e incluso puede causar errores de enlazador. Si su proceso de compilación usa un compilador de C normal para util.c, entonces necesitará usar "C" externa al incluir util.h.

Lo que sucede es que C ++ codifica los parámetros de la función en su nombre. Así es como funciona la sobrecarga de funciones. Todo lo que suele sucederle a una función C es agregar un guión bajo ("_") al comienzo del nombre. Sin usar "C" externa, el enlazador buscará una función llamada DoSomething @@ int @ float () cuando el nombre real de la función es _DoSomething () o simplemente DoSomething ().

El uso de "C" externa resuelve el problema anterior al decirle al compilador de C ++ que debe buscar una función que siga la convención de nomenclatura de C en lugar de la de C ++.


7

El compilador de C ++ crea nombres de símbolos de manera diferente que el compilador de C. Por lo tanto, si está intentando realizar una llamada a una función que reside en un archivo C, compilado como código C, debe decirle al compilador de C ++ que los nombres de los símbolos que está tratando de resolver tienen un aspecto diferente al predeterminado; de lo contrario, el paso del enlace fallará.


6

La extern "C" {}construcción le indica al compilador que no realice cambios en los nombres declarados entre llaves. Normalmente, el compilador de C ++ "mejora" los nombres de funciones para que codifiquen la información de tipo sobre argumentos y el valor de retorno; Esto se llama el nombre destrozado . losextern "C" construcción previene la destrucción.

Normalmente se usa cuando el código C ++ necesita llamar a una biblioteca de lenguaje C. También se puede usar al exponer una función C ++ (desde una DLL, por ejemplo) a clientes C.


5

Esto se utiliza para resolver problemas de cambio de nombre. extern C significa que las funciones están en una API de estilo C "plana".


0

Descompile un g++binario generado para ver qué está pasando

Para comprender por qué externes necesario, lo mejor que puede hacer es comprender lo que está sucediendo en detalle en los archivos de objetos con un ejemplo:

main.cpp

void f() {}
void g();

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

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

Compile con la salida GCC 4.8 Linux ELF :

g++ -c main.cpp

Descompilar la tabla de símbolos:

readelf -s main.o

La salida contiene:

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

Interpretación

Vemos eso:

  • efy egse almacenaron en símbolos con el mismo nombre que en el código

  • los otros símbolos fueron destrozados. Vamos a deshacerlos:

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

Conclusión: los dos tipos de símbolos siguientes no fueron destrozados:

  • definido
  • declarado pero indefinido ( Ndx = UND), que se proporcionará en el enlace o tiempo de ejecución desde otro archivo de objeto

Entonces necesitará extern "C"ambos cuando llame:

  • C de C ++: decir g++ que espere símbolos sin desencadenar producidos porgcc
  • C ++ de C: diga g++que genere símbolos sin desenvolver para gccusar

Cosas que no funcionan en el exterior C

Resulta obvio que cualquier característica de C ++ que requiera el cambio de nombre no funcionará en el interior 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) { }
}

Ejemplo mínimo de C ejecutable desde C ++

En aras de la exhaustividad y para los novatos, vea también: ¿Cómo usar archivos fuente C en un proyecto C ++?

Llamar a C desde C ++ es bastante fácil: cada función de C solo tiene un posible símbolo no mutilado, por lo que no se requiere trabajo adicional.

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

Correr:

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

Sin extern "C"el enlace falla con:

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

porque g++espera encontrar un destrozado f, que gccno produjo.

Ejemplo en GitHub .

Ejemplo de C ++ ejecutable mínimo desde C

Llamar a C ++ desde es un poco más difícil: tenemos que crear manualmente versiones no mutiladas de cada función que queremos exponer.

Aquí ilustramos cómo exponer sobrecargas de la función C ++ a C.

C Principal

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

Correr:

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

Sin extern "C"falla con:

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

porque g++generó símbolos destrozados que gccno pueden encontrar.

Ejemplo en GitHub .

Probado en Ubuntu 18.04.


1
Gracias por explicar el voto negativo, todo tiene sentido ahora.
Ciro Santilli 郝海东 冠状 病 六四 事件 法轮功
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.