Por que as funções C não podem ser alteradas pelo nome?

136

Eu tive uma entrevista recentemente e uma pergunta foi: qual é o uso do extern "C"código C ++. Eu respondi que é para usar funções C no código C ++, pois C não usa nomes diferentes. Perguntaram-me por que C não usa nomes desconfiados e, para ser sincero, não consegui responder.

Entendo que, quando o compilador C ++ compila funções, ele atribui um nome especial à função, principalmente porque podemos ter funções sobrecarregadas com o mesmo nome em C ++, que devem ser resolvidas em tempo de compilação. Em C, o nome da função permanecerá o mesmo, ou talvez com um _ antes dele.

Minha pergunta é: o que há de errado em permitir que o compilador C ++ também modifique as funções C? Eu teria assumido que não importa que nomes o compilador lhes dê. Chamamos funções da mesma maneira em C e C ++.

Engenheiro999
fonte
75
C não precisa alterar os nomes, porque não possui sobrecarga de função.
EOF
9
Como você vincula as bibliotecas C ao código C ++ se o compilador C ++ gerencia os nomes das funções?
Mat
6
"Eu respondi que é para usar funções C no código C ++, pois C não usa nomes diferentes." - Eu acho que é o contrário. Externo "C" torna as funções C ++ utilizáveis ​​em um compilador C. fonte
rozina
3
@ Engineer999: E se você compilar o subconjunto de C que também é C ++ com um compilador C ++, os nomes das funções serão realmente alterados. Mas se você deseja vincular binários criados com diferentes compiladores, não deseja desconfigurar nomes.
EOF
13
C altera nomes. Normalmente, o nome desconfigurado é o nome da função precedida por um sublinhado. Às vezes, é o nome da função seguido por um sublinhado. extern "C"diz para alterar o nome da mesma maneira que "o" compilador C faria.
Pete Becker

Respostas:

187

Foi meio que respondido acima, mas vou tentar colocar as coisas em contexto.

Primeiro, C veio primeiro. Como tal, o que C faz é, mais ou menos, o "padrão". Não altera nomes porque simplesmente não. Um nome de função é um nome de função. Um global é um global, e assim por diante.

Então C ++ apareceu. O C ++ queria poder usar o mesmo vinculador que C e vincular-se ao código escrito em C. Mas o C ++ não pôde deixar o C "confuso" (ou, falta dele) como está. Confira o seguinte exemplo:

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

No C ++, essas são funções distintas, com corpos distintos. Se nenhum deles for mutilado, ambos serão chamados de "função" (ou "_função") e o vinculador reclamará da redefinição de um símbolo. A solução C ++ foi alterar os tipos de argumento para o nome da função. Então, um é chamado _function_inte o outro é chamado _function_void(não é o esquema de confinamento real) e a colisão é evitada.

Agora ficamos com um problema. Se int function(int a)foi definido em um módulo C, e estamos apenas pegando seu cabeçalho (ou seja, declaração) no código C ++ e usando-o, o compilador gerará uma instrução para o vinculador importar _function_int. Quando a função foi definida, no módulo C, não foi chamada assim. Foi chamado _function. Isso causará um erro no vinculador.

Para evitar esse erro, durante a declaração da função, dizemos ao compilador que é uma função projetada para ser vinculada ou compilada por um compilador C:

extern "C" int function(int a);

O compilador C ++ agora sabe importar _functione não _function_int, e está tudo bem.

Shachar Shemesh
fonte
1
@ ShacharShamesh: Eu perguntei isso em outro lugar, mas, e quanto a vincular em bibliotecas compiladas em C ++? Quando o compilador está percorrendo e compilando meu código que chama uma das funções em uma biblioteca compilada em C ++, como ele sabe qual nome deve ser modificado ou atribuído à função ao apenas ver sua declaração ou chamada de função? Como saber que, onde é definido, é mutilado por nome para outra coisa? Portanto, deve haver um método padrão de separação de nomes em C ++?
Engineer999
2
Todo compilador faz isso da sua maneira especial. Se você estiver compilando tudo com o mesmo compilador, isso não importa. Mas se você tentar usar, digamos, uma biblioteca que foi compilada com o compilador da Borland, a partir de um programa que você está construindo com o compilador da Microsoft, bem ... boa sorte; você vai precisar dele :)
Mark VY
6
@ Engineer999 Você já se perguntou por que não existem bibliotecas portáteis em C ++, mas elas especificam exatamente qual versão (e sinalizadores) do compilador (e biblioteca padrão) você deve usar ou apenas exportam uma API C? Ai está. C ++ é praticamente a linguagem menos portátil já inventada, enquanto C é exatamente o oposto. Há esforços nesse sentido, mas por agora, se você quer algo que é verdadeiramente portátil você vai ficar com C.
Voo
1
@Voo Bem, em teoria, você deve escrever código portátil apenas aderindo ao padrão -std=c++11, por exemplo , e evitar o uso de qualquer coisa fora do padrão. É o mesmo que declarar uma versão Java (embora as versões mais recentes do Java sejam compatíveis com versões anteriores). Não é culpa dos padrões que as pessoas usam extensões específicas do compilador e código dependente da plataforma. Por outro lado, você não pode culpá-los, pois há muitas coisas (especialmente IO, como soquetes) ausentes no padrão. O comitê parece estar se aproximando lentamente disso. Corrija-me se eu perdi alguma coisa.
Mucaho 14/04
14
@mucaho: você está falando sobre portabilidade / compatibilidade de fontes. ou seja, a API. Voo está falando sobre compatibilidade binária , sem uma recompilação. Isso requer compatibilidade com ABI . Compiladores C ++ alteram regularmente sua ABI entre versões. (por exemplo, o g ++ nem tenta ter uma ABI estável. Suponho que eles não quebram a ABI apenas por diversão, mas não evitam mudanças que exijam uma alteração da ABI quando há algo a ser ganho e não há outra maneira boa para fazê-lo.).
Peter Cordes
45

Não é que eles "não possam", eles não sejam , em geral.

Se você deseja chamar uma função em uma biblioteca C chamada foo(int x, const char *y), não é bom permitir que o compilador C ++ faça isso foo_I_cCP()(ou seja o que for, apenas crie um esquema de manipulação aqui) apenas porque pode.

Esse nome não será resolvido, a função está em C e seu nome não depende da lista de tipos de argumentos. Portanto, o compilador C ++ precisa saber disso e marcar essa função como C para evitar a manipulação.

Lembre-se de que a função C pode estar em uma biblioteca cujo código-fonte você não possui, tudo o que você tem é o binário pré-compilado e o cabeçalho. Portanto, seu compilador C ++ não pode fazer "é coisa própria", afinal não pode mudar o que está na biblioteca.

descontrair
fonte
Esta é a parte que estou perdendo. Por que o compilador C ++ alteraria um nome de função quando vê apenas sua declaração ou quando é chamada. Ele não apenas modifica os nomes das funções quando vê sua implementação? Isso faria mais sentido para mim
Engineer999
13
@ Engineer999: Como você pode ter um nome para a definição e outro para a declaração? "Existe uma função chamada Brian que você pode chamar." "Ok, eu ligo para Brian." "Desculpe, não existe uma função chamada Brian." Acontece que se chama Graham.
Lightness Races in Orbit
Que tal vincular em bibliotecas compiladas em C ++? Quando o compilador está percorrendo e compilando nosso código que chama uma das funções em uma biblioteca compilada em C ++, como ele sabe qual nome deve ser modificado ou atribuído à função ao apenas ver sua declaração ou chamada de função?
Engineer999
1
@ Engineer999 Ambos devem concordar com o mesmo erro. Então eles veem o arquivo de cabeçalho (lembre-se, há muito poucos metadados nas DLLs nativas - os cabeçalhos são esses metadados) e dizem "Ah, certo, Brian deveria realmente ser Graham". Se isso não funcionar (por exemplo, com dois esquemas de manipulação incompatíveis), você não receberá um link correto e seu aplicativo falhará. O C ++ tem muitas incompatibilidades como essa. Na prática, você deve usar explicitamente o nome desconfigurado e desabilitar o desconforto do seu lado (por exemplo, você diz ao seu código para executar Graham, não Brian). Na prática real ... extern "C":)
Luaan
1
@ Engineer999 Posso estar errado, mas você talvez tenha experiência com linguagens como Visual Basic, C # ou Java (ou mesmo Pascal / Delphi até certo ponto)? Aqueles fazem a interoperabilidade parecer extremamente simples. Em C e especialmente C ++, é tudo menos. Há muitas convenções de chamada que você precisa honrar, precisa saber quem é responsável por qual memória e você deve ter os arquivos de cabeçalho que informam as declarações de função, já que as próprias DLLs não contêm informações suficientes - especialmente no caso de C. puro. Se você não possui um arquivo de cabeçalho, geralmente precisa descompilar a DLL para usá-lo.
Luaan
32

o que há de errado em permitir que o compilador C ++ também modifique as funções C?

Eles não seriam mais funções C.

Uma função não é apenas uma assinatura e uma definição; como uma função funciona é amplamente determinada por fatores como a convenção de chamada. A "Interface Binária do Aplicativo" especificada para uso em sua plataforma descreve como os sistemas se comunicam. A ABI do C ++ em uso pelo seu sistema especifica um esquema de manipulação de nomes, para que os programas nesse sistema saibam invocar funções nas bibliotecas e assim por diante. (Leia a ABI do C ++ Itanium para um ótimo exemplo. Você verá muito rapidamente por que é necessário.)

O mesmo se aplica à ABI C do seu sistema. Algumas ABIs C realmente têm um esquema de identificação de nomes (por exemplo, Visual Studio), portanto, trata-se menos de "desativar a identificação de nomes" e mais sobre como alternar da C ++ ABI para a C ABI, para determinadas funções. Marcamos funções C como sendo funções C, às quais o C ABI (em vez do C ++ ABI) é pertinente. A declaração deve corresponder à definição (seja no mesmo projeto ou em alguma biblioteca de terceiros), caso contrário, a declaração não faz sentido. Sem isso, seu sistema simplesmente não saberá como localizar / chamar essas funções.

Por que as plataformas não definem as ABIs C e C ++ iguais e se livram desse "problema", isso é parcialmente histórico - as AB AB originais não eram suficientes para C ++, que possui namespaces, classes e sobrecarga de operadores, todos dos quais precisam de alguma forma ser representados no nome de um símbolo de uma maneira amigável para o computador - mas também se pode argumentar que fazer com que os programas em C agora respeitem o C ++ seja injusto com a comunidade C, que teria que aturar uma complexidade muito mais complicada ABI apenas pelo interesse de outras pessoas que desejam interoperabilidade.

Raças de leveza em órbita
fonte
2
+int(PI/3), mas com um grão de sal: eu seria muito cauteloso ao falar de "C ++ ABI" ... AFAIK, existem tentativas de definir ABIs C ++, mas nenhum padrão real de fato / de jure - como isocpp.org/files /papers/n4028.pdf afirma (e eu concordo plenamente), citação, é profundamente irônico que o C ++ realmente sempre apoiou uma maneira de publicar uma API com uma ABI binária estável - recorrendo ao subconjunto C do C ++ via extern “C ”. . C++ Itanium ABIé apenas isso - alguns ++ ABI C para Itanium ... como discutido em stackoverflow.com/questions/7492180/c-abi-issues-list
3
@ vaxquis: Sim, não "ABI do C ++", mas "um ABI do C ++" da mesma maneira que eu tenho uma "chave da casa" que não funciona em todas as casas. Acho que poderia ser mais claro, embora eu tenha tentado deixar o mais claro possível, começando com a frase "A ABI do C ++ em uso pelo seu sistema " . Deixei o clarificador em frases posteriores por brevidade, mas vou aceitar uma edição que reduz a confusão aqui!
Lightness Races in Orbit
1
Os AIUI C abi tendiam a ser propriedade de uma plataforma, enquanto as C ++ ABIs tendiam a ser propriedade de um compilador individual e, muitas vezes, até mesmo a propriedade de uma versão individual de um compilador. Portanto, se você deseja vincular os módulos criados com diferentes ferramentas de fornecedores, precisará usar um C abi para a interface.
plugwash
A instrução "funções com nome desconfigurado não seriam mais funções C" é exagerada - é perfeitamente possível chamar funções com nome desconfigurado a partir da baunilha C simples se o nome desconfigurado for conhecido. O fato de o nome mudar não o torna menos aderente ao C ABI, ou seja, não o torna menos uma função C. Por outro lado, faz mais sentido - o código C ++ não poderia chamar uma função C sem declará-lo "C", porque faria diferença de nome ao tentar vincular-se ao receptor.
Peter - Restabelece Monica
@ PeterA.Schneider: Sim, a frase do título é exagerada. O restante da resposta contém os detalhes factuais pertinentes.
Lightness Races in Orbit
21

O MSVC de fato altera os nomes C, embora de maneira simples. Às vezes acrescenta @4ou outro número pequeno. Isso está relacionado às convenções de chamada e à necessidade de limpeza da pilha.

Portanto, a premissa é falha.

MSalters
fonte
2
Na verdade, esse não é um nome desconcertante. É simplesmente uma convenção de nomenclatura específica do fornecedor (ou adornamento de nomes) para impedir que problemas com executáveis ​​sejam vinculados a DLLs criadas com as funções que possuem convenções de chamada diferentes.
Peter
2
E quanto a anexar com a _?
OrangeDog
12
@ Peter: Literalmente a mesma coisa.
Lightness Races in Orbit
5
@Frankie_C: "O chamador limpa a pilha" não é especificado por nenhum padrão C: nenhuma convenção de chamada é mais padrão do que a outra na perspectiva do idioma.
Ben Voigt
2
E da perspectiva da MSVC, a "convenção de chamada padrão" é exatamente o que você escolhe /Gd, /Gr, /Gv, /Gz. (Ou seja, a convenção de chamada padrão é usada, a menos que uma declaração de função especifique explicitamente uma convenção de chamada). Você está pensando em __cdeclqual é a convenção de chamada padrão padrão.
precisa saber é o seguinte
13

É muito comum ter programas parcialmente escritos em C e parcialmente escritos em alguma outra linguagem (geralmente linguagem assembly, mas às vezes Pascal, FORTRAN ou outra coisa). Também é comum que os programas contenham componentes diferentes escritos por pessoas diferentes que talvez não tenham o código-fonte para tudo.

Na maioria das plataformas, existe uma especificação - geralmente chamada ABI [Application Binary Interface], que descreve o que um compilador deve fazer para produzir uma função com um nome específico que aceita argumentos de alguns tipos específicos e retorna um valor de algum tipo específico. Em alguns casos, uma ABI pode definir mais de uma "convenção de chamada"; Os compiladores para esses sistemas geralmente fornecem um meio de indicar qual convenção de chamada deve ser usada para uma função específica. Por exemplo, no Macintosh, a maioria das rotinas do Toolbox usa a convenção de chamada Pascal; portanto, o protótipo para algo como "LineTo" seria algo como:

/* 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);

Se todo o código em um projeto foi compilado usando o mesmo compilador, não importaria qual nome o compilador exportou para cada função, mas em muitas situações será necessário que o código C chame funções que foram compiladas usando outras ferramentas e não pode ser recompilado com o compilador atual [e pode muito bem nem estar em C]. Ser capaz de definir o nome do vinculador é, portanto, fundamental para o uso de tais funções.

supercat
fonte
Sim, essa é a resposta. Se for apenas C e C ++, é difícil entender por que isso é feito dessa maneira. Para entender, devemos colocar as coisas no contexto da velha maneira de vincular estaticamente. A vinculação estática parece primitiva para os programadores do Windows, mas é o principal motivo pelo qual C não pode alterar nomes.
user34660
2
@ user34660: Não existe. É a razão pela qual C não pode exigir a existência de recursos cuja implementação exigiria nomes exportáveis ​​confusos ou permitir a existência de vários símbolos com nomes semelhantes que se distinguem por características secundárias.
Supercat
sabemos que houve tentativas de "ordenar" essas coisas ou que essas extensões eram extensões disponíveis para C antes de C ++?
user34660
@ user34660: Re "O link estático parece primitivo para os programadores do Windows ...", mas o link dinâmico às vezes parece uma grande PITA para as pessoas que usam Linux, ao instalar o programa X (provavelmente escrito em C ++) significa ter que rastrear e instalar versões específicas de bibliotecas das quais você já possui versões diferentes no seu sistema.
Jamesqf #
@jamesqf, sim, o Unix não tinha vínculo dinâmico antes do Windows. Eu sei muito pouco sobre links dinâmicos no Unix / Linux, mas parece que não é tão fácil quanto poderia ser em um sistema operacional em geral.
user34660
12

Acrescentarei outra resposta, para abordar algumas das discussões tangenciais que ocorreram.

O C ABI (interface binária do aplicativo) originalmente pedia a passagem de argumentos na pilha na ordem inversa (ou seja, pressionada da direita para a esquerda), onde o chamador também libera o armazenamento da pilha. A ABI moderna realmente usa registradores para passar argumentos, mas muitas das considerações desconcertantes remontam à passagem original do argumento da pilha.

O Pascal ABI original, em contraste, empurrou os argumentos da esquerda para a direita, e o chamado teve que aparecer. O ABI C original é superior ao ABI Pascal original em dois pontos importantes. A ordem de envio de argumentos significa que o deslocamento da pilha do primeiro argumento é sempre conhecido, permitindo funções que possuem um número desconhecido de argumentos, onde os argumentos iniciais controlam quantos outros argumentos existem (ala printf).

A segunda maneira pela qual a C ABI é superior é o comportamento, caso o chamador e o destinatário não concordem com quantos argumentos existem. No caso C, desde que você não acesse argumentos anteriores ao último, nada de ruim acontece. Em Pascal, o número errado de argumentos é exibido na pilha e a pilha inteira está corrompida.

O ABI original do Windows 3.1 foi baseado em Pascal. Como tal, utilizou o Pascal ABI (argumentos da esquerda para a direita, chamados poplee). Como qualquer incompatibilidade no número do argumento pode levar à corrupção da pilha, um esquema confuso foi formado. Cada nome de função foi desconfigurado com um número indicando o tamanho, em bytes, de seus argumentos. Portanto, na máquina de 16 bits, a seguinte função (sintaxe C):

int function(int a)

Foi manipulado para function@2, porque inttem dois bytes de largura. Isso foi feito para que, se a declaração e a definição não coincidirem, o vinculador falhará ao encontrar a função em vez de corromper a pilha no tempo de execução. Por outro lado, se o programa for vinculado, você poderá ter certeza de que o número correto de bytes será exibido da pilha no final da chamada.

Windows de 32 bits e em diante use a stdcallABI. É semelhante ao Pascal ABI, exceto que a ordem de envio é como em C, da direita para a esquerda. Assim como a Pascal ABI, o nome mangling manipula o tamanho do byte de argumentos no nome da função para evitar a corrupção da pilha.

Diferentemente das declarações feitas em outros lugares aqui, o C ABI não altera os nomes das funções, mesmo no Visual Studio. Por outro lado, as funções de desconfiguração decoradas com a stdcallespecificação ABI não são exclusivas do VS. O GCC também suporta essa ABI, mesmo ao compilar para Linux. Isso é usado extensivamente pelo Wine , que usa seu próprio carregador para permitir a vinculação em tempo de execução dos binários compilados do Linux às DLLs compiladas do Windows.

Shachar Shemesh
fonte
9

Os compiladores C ++ usam nomes diferentes para permitir nomes de símbolos exclusivos para funções sobrecarregadas cuja assinatura seria a mesma. Ele também basicamente codifica os tipos de argumentos, o que permite o polimorfismo em um nível baseado em função.

C não exige isso, pois não permite a sobrecarga de funções.

Observe que o mangling de nome é um (mas certamente não é o único!) Motivo pelo qual não se pode confiar em uma 'C ++ ABI'.

dgrine
fonte
8

O C ++ deseja poder interoperar com o código C vinculado a ele ou com o qual está vinculado.

C espera nomes de funções sem nome.

Se o C ++ o manipulasse, ele não encontraria as funções não-mutiladas exportadas de C ou C não encontraria as funções exportadas pelo C ++. O vinculador C deve obter o nome que ele espera, porque não sabe que é proveniente ou está indo para o C ++.

Yakk - Adam Nevraumont
fonte
3

A manipulação incorreta dos nomes das funções e variáveis ​​C permitiria que seus tipos fossem verificados no momento do link. Atualmente, todas as implementações (?) C permitem definir uma variável em um arquivo e chamá-la como uma função em outro. Ou você pode declarar uma função com uma assinatura incorreta (por exemplo, void fopen(double)e depois chamá-la.

Propus um esquema para a ligação segura de tipo de variáveis ​​e funções C através do uso de manipulação em 1991. O esquema nunca foi adotado, porque, como outros já observaram aqui, isso destruiria a compatibilidade com versões anteriores.

Diomidis Spinellis
fonte
1
Você quer dizer "permitir que seus tipos sejam verificados no momento do link ". Os tipos são verificados no momento da compilação, mas a vinculação com nomes não manipulados não pode verificar se as declarações usadas nas diferentes unidades de compilação estão de acordo. E se eles não concordarem, é o seu sistema de criação que está fundamentalmente quebrado e precisa ser corrigido.
Cmaster - reinstate monica