Почему функции C не могут быть искажены по имени?

136

Недавно у меня было интервью, и был задан один вопрос: в чем смысл использования extern "C"кода на C ++? Я ответил, что это использование функций C в коде C ++, так как C не использует искажение имен. Меня спросили, почему C не использует искажение имен и, честно говоря, я не мог ответить.

Я понимаю, что когда компилятор C ++ компилирует функции, он дает специальное имя функции главным образом потому, что мы можем иметь перегруженные функции с тем же именем в C ++, которые должны быть разрешены во время компиляции. В C имя функции останется прежним, или, возможно, с _ перед ним.

Мой вопрос: что плохого в том, чтобы позволить компилятору C ++ также манипулировать функциями C? Я бы предположил, что не имеет значения, какие имена им дает компилятор. Мы вызываем функции одинаково в C и C ++.

Engineer999
источник
75
C не нужно искажать имена, потому что у него нет перегрузки функций.
EOF
9
Как связать библиотеки C с кодом C ++, если компилятор C ++ искажает имена функций?
Мат
6
«Я ответил, что это использование функций C в коде C ++, поскольку C не использует искажение имен». - Я думаю, что все наоборот. Extern "C" делает функции C ++ пригодными для использования в компиляторе C. источник
розина
3
@ Engineer999: И если вы скомпилируете подмножество C, которое также является C ++, с помощью компилятора C ++, имена функций действительно будут искажены. Но если вы хотите иметь возможность связывать двоичные файлы, созданные с помощью разных компиляторов, вам не нужно искажение имен.
EOF
13
C делает MANGLE имена. Обычно искаженное имя - это имя функции, которому предшествует подчеркивание. Иногда это имя функции с последующим подчеркиванием. extern "C"говорит искажать имя так же, как это делал бы компилятор Си.
Пит Беккер,

Ответы:

187

Это было своего рода ответом выше, но я постараюсь поместить вещи в контекст.

Сначала С пришел первым. Таким образом, то, что делает C, является своего рода «по умолчанию». Он не искажает имена, потому что это не так. Имя функции - это имя функции. Глобал есть глобал и так далее.

Затем появился C ++. C ++ хотел иметь возможность использовать тот же компоновщик, что и C, и иметь возможность ссылаться с кодом, написанным на C. Но C ++ не мог оставить C "искаженным" (или его отсутствием) как есть. Посмотрите на следующий пример:

int function(int a);
int function();

В C ++ это разные функции с разными телами. Если ни один из них не искажен, оба будут называться «function» (или «_function»), и компоновщик будет жаловаться на переопределение символа. Решением C ++ было преобразование типов аргументов в имя функции. Итак, один называется, _function_intа другой называется _function_void(не фактическая схема искажения), и столкновения избегают.

Теперь мы остались с проблемой. Если он int function(int a)был определен в модуле C, а мы просто берем его заголовок (то есть объявление) в коде C ++ и используем его, компилятор сгенерирует команду компоновщику для импорта _function_int. Когда функция была определена, в модуле C она не называлась так. Это было названо _function. Это приведет к ошибке компоновщика.

Чтобы избежать этой ошибки, во время объявления функции мы сообщаем компилятору, что это функция, предназначенная для связи или компиляции с помощью компилятора C:

extern "C" int function(int a);

C ++ компилятор знает теперь импортировать , _functionа не _function_int, и все хорошо.

Шахар Шемеш
источник
1
@ShacharShamesh: я спрашивал об этом в другом месте, но как насчет ссылок в скомпилированных библиотеках C ++? Когда компилятор просматривает и компилирует мой код, который вызывает одну из функций в скомпилированной библиотеке C ++, как он узнает, какое имя нужно изменить или дать функции, просто увидев ее объявление или вызов функции? Как узнать, что там, где оно определено, оно искажено чем-то другим? Таким образом, должен быть стандартный метод искажения имен в C ++?
Engineer999
2
Каждый компилятор делает это по-своему. Если вы компилируете все одним и тем же компилятором, это не имеет значения. Но если вы попытаетесь использовать, скажем, библиотеку, скомпилированную с помощью компилятора Borland, из программы, которую вы создаете с помощью компилятора Microsoft, что ж ... удачи; вам это понадобится :)
Mark VY
6
@ Engineer999 Когда-нибудь задумывались, почему не существует такой вещи, как переносимые библиотеки C ++, но они либо указывают, какую именно версию (и флаги) компилятора (и стандартной библиотеки) вы должны использовать, либо просто экспортируете C API? Вот и ты. C ++ - практически наименее переносимый язык, когда-либо изобретенный, в то время как C - полная противоположность. В этом отношении предпринимаются усилия, но сейчас, если вы хотите что-то действительно портативное, вы будете придерживаться C.
Voo
1
@ Voo Ну, теоретически вы должны быть в состоянии написать переносимый код, просто следуя, например -std=c++11, стандарту , и избегая использования чего-либо вне стандарта. Это то же самое, что объявлять версию Java (хотя более новые версии Java обратно совместимы). Это не ошибка стандартов, люди используют специфичные для компилятора расширения и зависимый от платформы код. С другой стороны, их нельзя винить, так как в стандарте не хватает многих вещей (особенно IO, например, сокетов). Комитет, похоже, медленно догоняет это. Поправь меня, если я что-то пропустил.
Мукахо,
14
@mucaho: вы говорите о переносимости / совместимости источников. т.е. API. Voo говорит о бинарной совместимости, без перекомпиляции. Это требует совместимости ABI . Компиляторы C ++ регулярно меняют свой ABI между версиями. (Например, g ++ даже не пытается иметь стабильный ABI. Я предполагаю, что они не нарушают ABI просто для удовольствия, но они не избегают изменений, которые требуют изменения ABI, когда есть что-то, что можно получить, и нет другого хорошего способа сделать это.).
Питер Кордес
45

Это не то, что они «не могут», они не являются , в общем.

Если вы хотите вызвать функцию из библиотеки C с именем foo(int x, const char *y), не стоит позволять вашему компилятору C ++ изменять это вfoo_I_cCP() (или что-то еще, просто составив схему искажения на месте) просто потому, что это возможно.

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

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

размотать
источник
Это та часть, по которой я скучаю. Почему компилятор C ++ искажает имя функции, когда видит только ее объявление или видит, что оно вызывается. Разве он не просто искажает имена функций, когда видит их реализацию? Это будет иметь больше смысла для меня
Engineer999
13
@ Engineer999: Как вы можете иметь одно имя для определения и другое для декларации? «Есть функция по имени Брайан, которую вы можете вызвать». «Хорошо, я позвоню Брайану». «Извините, нет функции с именем Brian». Оказывается, это называется Грэм.
Гонки легкости на орбите
А как насчет ссылок в скомпилированных библиотеках C ++? Когда компилятор просматривает и компилирует наш код, который вызывает одну из функций в скомпилированной библиотеке C ++, как он узнает, какое имя нужно изменить или дать функции, просто увидев ее объявление или вызов функции?
Engineer999
1
@ Engineer999 Оба должны договориться об одном и том же калечении. Поэтому они видят файл заголовка (помните, что в нативных DLL-файлах очень мало метаданных - это метаданные заголовков) и говорят: «Ах, да, Брайан действительно должен быть Грэмом». Если это не сработает (например, с двумя несовместимыми схемами искажения), вы не получите правильную ссылку, и ваше приложение потерпит неудачу. C ++ имеет много таких несовместимостей. На практике вы должны явно использовать искаженное имя и отключить искажение на своей стороне (например, вы говорите своему коду выполнять Грэма, а не Брайана). В реальной практике ... extern "C":)
Luaan
1
@ Engineer999 Возможно, я ошибаюсь, но, возможно, у вас есть опыт работы с такими языками, как Visual Basic, C # или Java (или даже с Pascal / Delphi)? Это делает взаимодействие очень простым. В C и особенно в C ++ это не что иное, как. Есть много соглашений о вызовах, которые вы должны соблюдать, вам нужно знать, кто за какую память отвечает, и у вас должны быть заголовочные файлы, которые сообщают вам объявления функций, поскольку сами библиотеки DLL не содержат достаточно информации - особенно в случае чистый C. Если у вас нет заголовочного файла, вам обычно нужно декомпилировать DLL, чтобы использовать его.
Luaan
32

что плохого в том, чтобы позволить компилятору C ++ также искажать функции C?

Они больше не были бы C-функциями.

Функция - это не просто подпись и определение; как работает функция, во многом определяется такими факторами, как соглашение о вызовах. «Двоичный интерфейс приложения», указанный для использования на вашей платформе, описывает, как системы взаимодействуют друг с другом. Интерфейс C ++ ABI, используемый вашей системой, определяет схему искажения имени, чтобы программы в этой системе знали, как вызывать функции в библиотеках и так далее. (Прочитайте C ++ Itanium ABI для отличного примера. Вы очень быстро поймете, почему это необходимо.)

То же самое относится к C ABI в вашей системе. Некоторые C ABI на самом деле имеют схему искажения имени (например, Visual Studio), поэтому речь идет не о «отключении искажения имени», а о переключении с C ++ ABI на C ABI для определенных функций. Мы помечаем функции C как функции C, к которым относится C ABI (а не C ++ ABI). Объявление должно соответствовать определению (будь то в том же проекте или в какой-нибудь сторонней библиотеке), иначе объявление не имеет смысла.Без этого ваша система просто не будет знать, как найти / вызвать эти функции.

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

Гонки легкости на орбите
источник
2
+int(PI/3), но с небольшим количеством соли: я бы очень осторожно говорил о "C ++ ABI" ... AFAIK, есть попытки определить C ++ ABI, но нет реальных стандартов де-факто / де-юре - как isocpp.org/files /papers/n4028.pdf утверждает (и я искренне согласен), цитата : глубоко иронично, что C ++ всегда всегда поддерживал способ публикации API со стабильным двоичным ABI - прибегая к подмножеству C в C ++ через extern «C ». , C++ Itanium ABIэто всего лишь некоторый C ++ ABI для Itanium ... как обсуждено на stackoverflow.com/questions/7492180/c-abi-issues-list
3
@vaxquis: Да, не "ABI C ++", а "ABI C ++" так же, как у меня есть "ключ от дома", который не работает в каждом доме. Думаю, это может быть понятнее, хотя я попытался сделать это как можно более ясным, начав с фразы «ABI C ++, используемый вашей системой » . Для краткости я уронил осветлитель в более поздних высказываниях, но я приму правку, которая уменьшит путаницу здесь!
Гонки легкости на орбите
1
AIUI C abi, как правило, являются свойством платформы, в то время как C ++ ABI, как правило, являются свойством отдельного компилятора и часто даже свойством отдельной версии компилятора. Поэтому, если вы хотите связать модули, созданные с помощью инструментов разных производителей, вам нужно было использовать C abi для интерфейса.
plugwash
Утверждение «искаженные по имени функции больше не будут функциями C» - вполне возможно вызывать искаженные по имени функции из простого ванильного C, если искаженное имя известно. То, что изменение имени не делает его менее привязанным к C ABI, то есть не делает его менее функцией C. Обратный путь имеет больше смысла - код C ++ не может вызывать функцию C, не объявляя ее «C», потому что он будет делать искажение имени при попытке связать с вызываемым объектом.
Питер - Восстановить Монику
@ PeterA.Schneider: Да, заголовок фразы преувеличен. Вся остальная часть ответа содержит соответствующую фактическую деталь.
Гонки легкости на орбите
21

MSVC фактически делает MANGLE имена C, хотя в простой форме. Иногда добавляется @4или другое небольшое число. Это относится к соглашениям о вызовах и необходимости очистки стека.

Так что предпосылка просто ошибочна.

MSalters
источник
2
Это не совсем название искажения. Это просто соглашение о присвоении имен (или добавление имен), определяемое поставщиком, чтобы предотвратить проблемы с исполняемыми файлами, связанными с DLL, созданными с функциями, имеющими различные соглашения о вызовах.
Питер
2
Как насчет того, чтобы договориться с _?
OrangeDog
12
@Peter: Буквально то же самое.
Гонки легкости на орбите
5
@Frankie_C: «Вызывающий очищает стек» не определено ни одним стандартом C: ни одно соглашение о вызовах не является более стандартным, чем другое с точки зрения языка.
Бен Фойгт
2
А с точки зрения MSVC «стандартное соглашение о вызовах» - это то, что вы выбираете /Gd, /Gr, /Gv, /Gz. (То есть, используется стандартное соглашение о вызовах, если только объявление функции явно не определяет соглашение о вызове.). Вы думаете о том, __cdeclкакое стандартное соглашение о вызовах по умолчанию.
MSalters
13

Очень часто есть программы, которые частично написаны на C и частично написаны на каком-то другом языке (часто на ассемблере, но иногда на Pascal, FORTRAN или чем-то еще). Также часто программы содержат разные компоненты, написанные разными людьми, которые могут не иметь исходного кода для всего.

На большинстве платформ существует спецификация, часто называемая ABI [Application Binary Interface], которая описывает, что должен делать компилятор для создания функции с определенным именем, которая принимает аргументы некоторых определенных типов и возвращает значение некоторого определенного типа. В некоторых случаях ABI может определять более одного «соглашения о вызовах»; Компиляторы для таких систем часто предоставляют средства указания, какое соглашение о вызовах следует использовать для конкретной функции. Например, в Macintosh большинство подпрограмм Toolbox используют соглашение о вызовах Pascal, поэтому прототип для чего-то вроде «LineTo» будет выглядеть примерно так:

/* Note that there are no underscores before the "pascal" keyword because
   the Toolbox was written in the early 1980s, before the Standard and its
   underscore convention were published */
pascal void LineTo(short x, short y);

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

Supercat
источник
Да, это ответ. Если это просто C и C ++, тогда трудно понять, почему это так. Чтобы понять, мы должны поместить вещи в контекст старого способа статического связывания. Статическое связывание кажется примитивным для программистов Windows, но это основная причина, по которой С не может искажать имена.
user34660
2
@ user34660: Не очень. Это причина того, что C не может предписывать существование функций, реализация которых потребует либо искажения экспортируемых имен, либо разрешения существования нескольких одноименных символов, которые различаются вторичными характеристиками.
суперкат
Известно ли нам, что были попытки «поручить» такие вещи или что такие вещи были расширениями, доступными для C до C ++?
user34660 15.04.16
@ user34660: Re "Статическое связывание кажется примитивным для программистов Windows ...", но динамическое связывание иногда кажется основной PITA для людей, использующих Linux, при установке программы X (вероятно, написанной на C ++) необходимо отслеживать и устанавливать определенные версии библиотек, которые у вас уже есть разные версии в вашей системе.
jamesqf
@jamesqf, да, Unix не имел динамического связывания до Windows. Я очень мало знаю о динамическом линковании в Unix / Linux, но похоже, что оно не так гладко, как в операционной системе в целом.
user34660
12

Я добавлю еще один ответ, чтобы ответить на некоторые из обсуждений, которые имели место.

C ABI (двоичный интерфейс приложения) первоначально вызывал передачу аргументов в стеке в обратном порядке (т. Е. Толкает справа налево), где вызывающая сторона также освобождает хранилище стека. Современный ABI фактически использует регистры для передачи аргументов, но многие из искажающих соображений восходят к передаче оригинального стека.

Оригинальный ABI Pascal, напротив, выдвигал аргументы слева направо, и вызываемый должен был выдвигать аргументы. Оригинальный C ABI превосходит оригинальный Pascal ABI в двух важных моментах. Порядок проталкивания аргументов означает, что смещение стека первого аргумента всегда известно, что позволяет функциям с неизвестным числом аргументов, где ранние аргументы управляют количеством других аргументов (alaprintf ).

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

Оригинальный Windows 3.1 ABI был основан на Pascal. Как таковой, он использовал Паскаль ABI (аргументы в порядке слева направо, Callee Pops). Поскольку любое несоответствие номера аргумента может привести к повреждению стека, была сформирована схема искажения. Каждое имя функции было искажено числом, указывающим размер в байтах его аргументов. Итак, на 16-битной машине, следующая функция (синтаксис C):

int function(int a)

Был искалечен function@2 , потому чтоint имеет ширину два байта. Это было сделано для того, чтобы в случае несоответствия объявления и определения компоновщик не смог найти функцию, а не повредил стек во время выполнения. И наоборот, если программа соединяется, то вы можете быть уверены, что в конце вызова выбрано правильное количество байтов из стека.

32-битная Windows и далее использовать stdcall вместо этого ABI. Это похоже на Паскаль ABI, за исключением того, что порядок нажатия такой же, как в C, справа налево. Как и в Паскаль ABI, искажение имени меняет размер байтов аргументов в имя функции, чтобы избежать повреждения стека.

В отличие от утверждений, сделанных в другом месте, C ABI не искажает имена функций, даже в Visual Studio. И наоборот, функции искажения, украшенные stdcallспецификацией ABI, не уникальны для VS. GCC также поддерживает этот ABI, даже при компиляции для Linux. Это широко используется Wine , который использует собственный загрузчик, чтобы разрешить связывание исполняемых файлов Linux скомпилированных библиотек Windows во время выполнения.

Шахар Шемеш
источник
9

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

C не требует этого, поскольку не допускает перегрузки функций.

Обратите внимание, что искажение имен является одной (но, конечно, не единственной!) Причиной, по которой нельзя полагаться на C ++ ABI.

dgrine
источник
8

C ++ хочет иметь возможность взаимодействовать с кодом C, который ссылается на него или на который он ссылается.

C ожидает неименованные искаженные имена функций.

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

Якк - Адам Невраумонт
источник
3

Изменение имен функций и переменных Си позволило бы проверять их типы во время соединения. В настоящее время все (?) Реализации C позволяют вам определять переменную в одном файле и вызывать ее как функцию в другом. Или вы можете объявить функцию с неправильной подписью (например, void fopen(double)а затем вызвать ее).

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

Диомидис Спинеллис
источник
1
Вы имеете в виду «разрешить проверку их типов во время ссылки ». Типы которые проверяются во время компиляции, но связь с unmangled имен не может проверить , является ли согласны заявления , используемые в различных единицах компиляции. И если они не согласны, это ваша система сборки, которая в корне сломана и должна быть исправлена.
cmaster - восстановить монику