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

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

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

Конкретно:

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

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

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

Стоит ли изучать PHP в 2026-2027 годах?
Стоит ли изучать PHP в 2026-2027 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
138
0
83 303
11
Перейти к ответу Данный вопрос помечен как решенный

Ответы 11

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

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

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

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

Ответ принят как подходящий

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

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

Bulat M. 28.09.2016 21:39

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

Calmarius 13.03.2017 15:58

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

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

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

extern «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++, конечно, должен быть искажен, иначе он не будет работать.

When should we use it?

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

What is happening at the compiler/linker level that requires us to use it?

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

How in terms of compilation/linking does this solve the problems which require us to use it?

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

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

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

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

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

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

Спасибо за объяснение отрицательного голоса, теперь все имеет смысл.

Ciro Santilli TRUMP BAN IS BAD 19.08.2019 23:23

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