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

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

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

В частности:

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

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

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

Ответ 1

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

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

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

Ответ 2

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

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

#ifdef __cplusplus
extern "C" {
#endif

... declarations ...

#ifdef __cplusplus
}
#endif

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

Ответ 3

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

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

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

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

Ответ 4

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

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

Ответ 5

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

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

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

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

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

Ответ 6

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

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

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

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

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

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

Ответ 7

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

Ответ 8

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

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

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

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

Ответ 9

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

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

Ответ 10

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

Ответ 11

Декомпилируйте сгенерированный двоичный файл 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

к.ц

#include "c.h"

int f(void) { return 1; }

Run:

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++ from немного сложнее: нам нужно вручную создавать не искаженные версии каждой функции, которую мы хотим представить.

Здесь мы проиллюстрируем, как показать перегрузки функции 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);
}

Run:

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.