Зачем нам нужен extern «C» {#include <foo.h>} в C ++?


136

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

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

В частности:

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

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

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

Ответы:


122

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

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

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


1
Не могли бы вы подробнее остановиться на том, что «большинство системных заголовков C уже учитывают тот факт, что они могут быть включены в код C ++ и уже вне их кода». ?
Булат М.

7
@BulatM. Они содержат что-то вроде этого: #ifdef __cplusplus extern "C" { #endif поэтому при включении из файла C ++ они по-прежнему рассматриваются как заголовок C.
Кальмариус

111

extern "C" определяет, как символы в сгенерированном объектном файле должны быть названы. Если функция объявлена ​​без внешнего «C», имя символа в объектном файле будет использовать искажение имени 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, если он старый и у него нет этих охранников.


22

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

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

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

extern "C" говорит компилятору C ++ не выполнять никакого искажения имени в коде в фигурных скобках. Это позволяет вам вызывать функции C изнутри C ++.


14

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

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


11

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

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

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

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

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


11

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

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

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

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

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

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


7

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

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

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

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


7

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


6

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

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


5

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


0

Декомпилируйте 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использования

Вещи, которые не работают в extern 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);
}

ч

#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

куб.см

#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.


1
Спасибо за объяснение downvote, теперь все это имеет смысл.
Сиро Сантилли 郝海东 冠状 病 六四 事件 法轮功
Используя наш сайт, вы подтверждаете, что прочитали и поняли нашу Политику в отношении файлов cookie и Политику конфиденциальности.
Licensed under cc by-sa 3.0 with attribution required.