it-swarm-ru.tech

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

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

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

В частности:

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

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

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

129
Landon

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 - их код.

117
duane

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

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

#ifdef __cplusplus
extern "C" {
#endif

... declarations ...

#ifdef __cplusplus
}
#endif

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

107
Todd Gamblin

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

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

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

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

21
Trent

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

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

14
1800 INFORMATION

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

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

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

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

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

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

11
Tony M

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

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

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

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

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

10
tialaramex

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

7
mbyrne215

Вы должны использовать 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
HitScan

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

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

6
Paul Lalonde

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

5
Eric Z Beard

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

Я перехожу к этому ответу из: Каково влияние extern "C" в C++? , поскольку этот вопрос считался дубликатом этого.

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

Бежать:

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.