Зачем нам нужен extern C {#include ‹foo.h›} в C ++?

Зачем нужно использовать:

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

В частности:

  • Когда мы должны его использовать?

  • Что происходит на уровне компилятора / компоновщика, что требует от нас его использования?

  • Как с точки зрения компиляции / связывания это решает проблемы, которые требуют от нас его использования?


person Landon    schedule 15.09.2008    source источник


Ответы (11)


C и C ++ внешне похожи, но каждый компилируется в совершенно другой набор кода. Когда вы включаете файл заголовка с компилятором C ++, компилятор ожидает код C ++. Если, однако, это заголовок C, то компилятор ожидает, что данные, содержащиеся в файле заголовка, будут скомпилированы в определенный формат - C ++ «ABI» или «Application Binary Interface», поэтому компоновщик захлебнется. Это предпочтительнее передачи данных C ++ функции, ожидающей данные C.

(Чтобы вникнуть в подробности, ABI C ++ обычно `` искажает '' имена своих функций / методов, поэтому, вызывая printf() без пометки прототипа как функции C, C ++ фактически сгенерирует код, вызывающий _Zprintf, плюс дополнительную хрень в конец.)

Итак: используйте extern "C" {...} при включении заголовка c - это так просто. В противном случае у вас будет несоответствие в скомпилированном коде, и компоновщик задохнется. Однако для большинства заголовков вам даже не понадобится extern, потому что большинство системных заголовков C уже учитывают тот факт, что они могут быть включены кодом C ++ и уже extern их кодом.

person duane    schedule 15.09.2008
comment
Не могли бы вы подробнее рассказать о том, большинство системных заголовков C уже учитывают тот факт, что они могут быть включены в код C ++ и уже извлекают из своего кода.? - person Bulat M.; 28.09.2016
comment
@BulatM. Они содержат что-то вроде этого: #ifdef __cplusplus extern "C" { #endif Поэтому, когда они включены из файла C ++, они по-прежнему обрабатываются как заголовок C. - person Calmarius; 13.03.2017

extern «C» определяет, как должны называться символы в созданном объектном файле. Если функция объявлена ​​без extern «C», имя символа в объектном файле будет использовать изменение имени C ++. Вот пример.

Данный test.C так:

void foo() { }

Компиляция и перечисление символов в объектном файле дает:

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

Функция foo на самом деле называется "_Z3foov". Эта строка, помимо прочего, содержит информацию о типе возвращаемого типа и параметров. Если вместо этого вы напишете test.C следующим образом:

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

Затем скомпилируйте и посмотрите на символы:

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

Вы получаете связь C. Имя функции «foo» в объектном файле - просто «foo», и в нем нет всей информации о причудливом типе, полученной в результате изменения имени.

Обычно вы включаете заголовок в extern «C» {}, если код, который идет с ним, был скомпилирован с помощью компилятора C, но вы пытаетесь вызвать его из C ++. Когда вы это делаете, вы сообщаете компилятору, что все объявления в заголовке будут использовать связь C. Когда вы связываете свой код, ваши файлы .o будут содержать ссылки на «foo», а не на «_Z3fooblah», что, надеюсь, соответствует тому, что находится в библиотеке, с которой вы связываете.

Большинство современных библиотек будут защищать такие заголовки, чтобы символы объявлялись с правильной связью. например во многих стандартных заголовках вы найдете:

#ifdef __cplusplus
extern "C" {
#endif

... declarations ...

#ifdef __cplusplus
}
#endif

Это гарантирует, что когда код C ++ включает заголовок, символы в вашем объектном файле соответствуют тому, что находится в библиотеке C. Вы должны помещать extern "C" {} вокруг заголовка C только в том случае, если он старый и уже не имеет этих средств защиты.

person Todd Gamblin    schedule 15.09.2008

В C ++ у вас могут быть разные сущности с одинаковым именем. Например, вот список функций с именем foo:

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

Чтобы различать их всех, компилятор C ++ создаст уникальные имена для каждого в процессе, называемом изменением имен или украшением. Компиляторы C этого не делают. Более того, каждый компилятор C ++ может делать это по-своему.

extern «C» сообщает компилятору C ++ не выполнять никаких изменений имен в коде, заключенном в фигурные скобки. Это позволяет вам вызывать функции C из C ++.

person Trent    schedule 15.09.2008

Это связано с тем, как разные компиляторы изменяют имена. Компилятор C ++ будет искажать имя символа, экспортированного из файла заголовка, совершенно иначе, чем компилятор C, поэтому при попытке связать вы получите ошибку компоновщика, сообщающую, что отсутствовали символы.

Чтобы решить эту проблему, мы говорим компилятору C ++ работать в режиме «C», чтобы он выполнял изменение имени таким же образом, как и компилятор C. После этого ошибки компоновщика исправлены.

person 1800 INFORMATION    schedule 15.09.2008

В C и C ++ действуют разные правила имен символов. С помощью символов компоновщик узнает, что вызов функции openBankAccount в одном объектном файле, созданном компилятором, является ссылкой на функцию, которую вы назвали openBankAccount в другом объектном файле, созданном из другого исходного файла тем же (или совместимым) компилятор. Это позволяет вам создавать программу из более чем одного исходного файла, что является облегчением при работе над большим проектом.

В C правило очень простое: все символы в любом случае находятся в одном пространстве имен. Таким образом, целое число «socks» сохраняется как «socks», а функция count_socks сохраняется как «count_socks».

Линкеры были созданы для C и других языков, таких как C, с этим простым правилом именования символов. Таким образом, символы в компоновщике - это простые строки.

Но в C ++ язык позволяет иметь пространства имен, полиморфизм и другие вещи, которые противоречат такому простому правилу. Все шесть ваших полиморфных функций, называемых «add», должны иметь разные символы, иначе другие объектные файлы будут использовать неправильный. Это делается путем «искажения» (это технический термин) имен символов.

При связывании кода C ++ с библиотеками или кодом C вам понадобится extern «C», что-либо, написанное на C, например файлы заголовков для библиотек C, чтобы сообщить компилятору C ++, что эти имена символов не должны изменяться, в то время как остальная часть ваш код C ++, конечно, должен быть искажен, иначе он не будет работать.

person tialaramex    schedule 15.09.2008

Когда мы должны его использовать?

Когда вы связываете библиотеки C с объектными файлами C ++

Что происходит на уровне компилятора / компоновщика, что требует от нас его использования?

C и C ++ используют разные схемы именования символов. Это указывает компоновщику использовать схему C при компоновке в данной библиотеке.

Как с точки зрения компиляции / связывания это решает проблемы, которые требуют от нас его использования?

Использование схемы именования C позволяет ссылаться на символы C-стиля. В противном случае компоновщик попробовал бы символы в стиле C ++, которые не сработали бы.

person Tony M    schedule 15.09.2008

Вы должны использовать extern «C» каждый раз, когда вы включаете заголовок, определяющий функции, находящиеся в файле, скомпилированном компилятором C, используемом в файле C ++. (Многие стандартные библиотеки C могут включать эту проверку в свои заголовки, чтобы упростить для разработчика)

Например, если у вас есть проект с 3 файлами, util.c, util.h и main.cpp, и файлы .c и .cpp скомпилированы с помощью компилятора C ++ (g ++, cc и т. Д.), Тогда это не так. t действительно необходимо, и может даже вызвать ошибки компоновщика. Если ваш процесс сборки использует обычный компилятор C для util.c, тогда вам нужно будет использовать extern «C» при включении util.h.

Что происходит, так это то, что C ++ кодирует параметры функции в своем имени. Так работает перегрузка функций. Все, что обычно происходит с функцией C, - это добавление символа подчеркивания («_») в начало имени. Без использования extern «C» компоновщик будет искать функцию с именем DoSomething @@ int @ float (), когда фактическое имя функции _DoSomething () или просто DoSomething ().

Использование extern «C» решает указанную выше проблему, сообщая компилятору C ++, что он должен искать функцию, которая следует соглашению об именах C, а не C ++.

person HitScan    schedule 15.09.2008

Компилятор C ++ создает имена символов иначе, чем компилятор C. Итак, если вы пытаетесь вызвать функцию, которая находится в файле C, скомпилированном как код C, вам необходимо сообщить компилятору C ++, что имена символов, которые он пытается разрешить, выглядят иначе, чем по умолчанию; в противном случае шаг ссылки завершится ошибкой.

person mbyrne215    schedule 15.09.2008

Конструкция extern "C" {} указывает компилятору не изменять имена, объявленные в фигурных скобках. Обычно компилятор C ++ «расширяет» имена функций, чтобы они кодировали информацию о типе аргументов и возвращаемое значение; это называется искаженным именем. Конструкция extern "C" предотвращает искажение.

Обычно он используется, когда код C ++ должен вызвать библиотеку языка C. Его также можно использовать при предоставлении функции C ++ (например, из библиотеки DLL) клиентам C.

person Paul Lalonde    schedule 15.09.2008

Это используется для решения проблем с искажением имен. extern C означает, что функции находятся в «плоском» API в стиле C.

person Eric Z Beard    schedule 15.09.2008

Декомпилируйте g++ сгенерированный двоичный файл, чтобы увидеть, что происходит

Чтобы понять, зачем нужен extern, лучше всего понять, что происходит в объектных файлах в деталях, на примере:

main.cpp

void f() {}
void g();

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

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

Скомпилируйте с GCC 4.8 Linux Вывод в формате ELF:

g++ -c main.cpp

Декомпилируйте таблицу символов:

readelf -s main.o

Вывод содержит:

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

Устный перевод

Мы видим, что:

  • ef и eg хранились в символах с тем же именем, что и в коде

  • другие символы были искажены. Распутаем их:

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

Вывод: оба следующих типа символов не искажены:

  • определенный
  • объявлен, но не определен (Ndx = UND), должен быть предоставлен во время ссылки или во время выполнения из другого объектного файла

Таким образом, при звонке вам понадобятся extern "C" оба:

  • C из C ++: укажите g++, что следует ожидать несвязанных символов, созданных gcc
  • C ++ из C: сообщить g++ о создании несвязанных символов для gcc, которые будут использоваться

Что не работает во внешнем C

Становится очевидным, что любая функция C ++, требующая изменения имени, не будет работать внутри 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) { }
}

Минимальный исполняемый C из примера C ++

Для полноты картины и для новичков см. Также: Как использовать исходные файлы C в проекте C ++?

Вызвать C из C ++ довольно просто: каждая функция C имеет только один возможный неискаженный символ, поэтому никаких дополнительных действий не требуется.

main.cpp

#include <cassert>

#include "c.h"

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

c.h

#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

c.c

#include "c.h"

int f(void) { return 1; }

Бегать:

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

Без extern "C" ссылка не работает:

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

потому что g++ ожидает найти искалеченный f, который gcc не дал.

Пример на GitHub.

Минимальный исполняемый C ++ из примера C

Вызов C ++ из немного сложнее: мы должны вручную создавать неискаженные версии каждой функции, которую мы хотим предоставить.

Здесь мы проиллюстрируем, как открыть доступ к перегрузкам функций C ++ для C.

main.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);
}

Бегать:

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

Без extern "C" он не работает с:

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

потому что g++ генерирует искаженные символы, которые gcc не могут найти.

Пример на GitHub.

Протестировано в Ubuntu 18.04.

person Ciro Santilli 新疆再教育营六四事件ۍ    schedule 14.05.2019
comment
Спасибо за объяснение отрицательного голоса, теперь все имеет смысл. - person Ciro Santilli 新疆再教育营六四事件ۍ 19.08.2019