Вопрос

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

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

Конкретно:

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

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

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

Это было полезно?

Решение

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

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

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

Другие советы

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 только в том случае, если он старый и еще не имеет этих защит.

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Декомпилировать 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 ЭЛЬФ выход:

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" оба при вызове:

  • С из С++:рассказывать g++ ожидать неповрежденных символов, созданных gcc
  • С++ из С:рассказывать 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);
}

ч.ч.

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

Лицензировано под: CC-BY-SA с атрибуция
Не связан с StackOverflow
scroll top