Perché le funzioni C non possono essere alterate dal nome?


136

Ho avuto un'intervista di recente e una domanda è stata: a che serve il extern "C"codice C ++. Ho risposto che si tratta di utilizzare le funzioni C nel codice C ++ poiché C non usa la modifica del nome. Mi è stato chiesto perché C non usa il logoramento dei nomi e ad essere sincero non ho potuto rispondere.

Capisco che quando il compilatore C ++ compila le funzioni, dà un nome speciale alla funzione principalmente perché possiamo avere funzioni sovraccariche con lo stesso nome in C ++ che devono essere risolte al momento della compilazione. In C, il nome della funzione rimarrà lo stesso, o forse con un _ prima di esso.

La mia domanda è: cosa c'è di sbagliato nel consentire al compilatore C ++ di gestire anche le funzioni C? Avrei supposto che non importa quali nomi il compilatore dà loro. Chiamiamo le funzioni allo stesso modo in C e C ++.


75
C non ha bisogno di manipolare i nomi, perché non ha un sovraccarico di funzioni.
EOF,

9
Come si collegano le librerie C con il codice C ++ se il compilatore C ++ modifica i nomi delle funzioni?
Mat

6
"Ho risposto che si tratta di utilizzare le funzioni C nel codice C ++ poiché C non usa la modifica del nome." - Penso che sia il contrario. "C" esterno rende le funzioni C ++ utilizzabili in un compilatore C. fonte
rozina,

3
@ Engineer999: E se si compila il sottoinsieme di C che è anche C ++ con un compilatore C ++, i nomi delle funzioni verranno effettivamente alterati. Ma se vuoi essere in grado di collegare i binari creati con diversi compilatori, non vuoi alterare il nome.
EOF

13
C fa i nomi mangle. In genere il nome alterato è il nome della funzione preceduto da un trattino basso. A volte è il nome della funzione seguito da un trattino basso. extern "C"dice di manipolare il nome nello stesso modo in cui "il" compilatore C farebbe.
Pete Becker,

Risposte:


187

È stata una specie di risposta sopra, ma cercherò di mettere le cose nel contesto.

In primo luogo, C è arrivato per primo. In quanto tale, ciò che fa C è, in un certo senso, il "default". Non manipola i nomi perché non lo fa. Un nome di funzione è un nome di funzione. Un globale è un globale e così via.

Quindi è arrivato il C ++. Il C ++ voleva essere in grado di usare lo stesso linker di C, e di essere in grado di collegarsi con il codice scritto in C. Ma C ++ non poteva lasciare la "mangling" (o la mancanza di) C così com'è. Guarda il seguente esempio:

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

In C ++, queste sono funzioni distinte, con corpi distinti. Se nessuno di essi è alterato, entrambi saranno chiamati "funzione" (o "_funzione") e il linker si lamenterà della ridefinizione di un simbolo. La soluzione C ++ consisteva nel manipolare i tipi di argomento nel nome della funzione. Quindi, uno viene chiamato _function_inte l'altro viene chiamato _function_void(non vero schema di demolizione) e la collisione viene evitata.

Ora ci resta un problema. Se è int function(int a)stato definito in un modulo C, e stiamo semplicemente prendendo la sua intestazione (cioè la dichiarazione) nel codice C ++ e lo stiamo usando, il compilatore genererà un'istruzione per il linker da importare _function_int. Quando la funzione è stata definita, nel modulo C, non è stata chiamata così. Si chiamava _function. Ciò causerà un errore del linker.

Per evitare questo errore, durante la dichiarazione della funzione, diciamo al compilatore che è una funzione progettata per essere collegata o compilata da un compilatore C:

extern "C" int function(int a);

Il compilatore C ++ ora sa importare _functionpiuttosto che _function_int, e tutto va bene.


1
@ShacharShamesh: l'ho chiesto altrove, ma per quanto riguarda il collegamento nelle librerie compilate in C ++? Quando il compilatore sta esaminando e compilando il mio codice che chiama una delle funzioni in una libreria compilata in C ++, come fa a sapere quale nome manipolare o dare alla funzione solo vedendo la sua dichiarazione o chiamata di funzione? Come sapere che dove è definito, è ingannato da qualcos'altro? Quindi ci deve essere un metodo standard di modifica del nome in C ++?
Ingegnere999

2
Ogni compilatore lo fa a modo suo. Se stai compilando tutto con lo stesso compilatore, non importa. Ma se provi ad usare, per esempio, una libreria che è stata compilata con il compilatore di Borland, da un programma che stai costruendo con il compilatore di Microsoft, beh ... buona fortuna; ne avrai bisogno :)
Mark VY

6
@ Engineer999 Ti sei mai chiesto perché non esistono librerie C ++ portatili, ma specificano esattamente quale versione (e flag) del compilatore (e libreria standard) devi usare o semplicemente esporti un'API C? Ecco qua Il C ++ è praticamente il linguaggio meno portatile mai inventato, mentre il C è esattamente l'opposto. Ci sono sforzi in tal senso, ma per ora se vuoi qualcosa che sia veramente portatile rimarrai con C.
Voo

1
@Voo Beh, in teoria dovresti essere in grado di scrivere codice portatile semplicemente aderendo allo standard -std=c++11, ad esempio , ed evitare l'uso di qualsiasi cosa al di fuori dello standard. È lo stesso che dichiarare una versione Java (anche se le versioni Java più recenti sono retrocompatibili). Non è colpa degli standard che le persone usano estensioni specifiche del compilatore e codice dipendente dalla piattaforma. D'altra parte, non è possibile biasimarli, poiché nello standard mancano molte cose (specialmente IO, come socket). Il comitato sembra essersi lentamente avvicinato a questo. Correggimi se ho perso qualcosa.
mucaho,

14
@mucaho: stai parlando di portabilità / compatibilità dei sorgenti. cioè l'API. Voo sta parlando di compatibilità binaria , senza una ricompilazione. Ciò richiede la compatibilità ABI . I compilatori C ++ cambiano regolarmente la loro ABI tra le versioni. (es. g ++ non prova nemmeno ad avere un ABI stabile. Presumo che non rompano l'ABI solo per divertimento, ma non evitano i cambiamenti che richiedono un cambiamento ABI quando c'è qualcosa da guadagnare e nessun altro buon modo per farlo.).
Peter Cordes,

45

Non è che "non possono", non lo sono , in generale.

Se vuoi chiamare una funzione in una libreria C chiamata foo(int x, const char *y), non va bene lasciare che il tuo compilatore C ++ lo inserisca foo_I_cCP()(o qualsiasi altra cosa, abbia appena creato uno schema di demolizione sul posto qui) solo perché può.

Quel nome non si risolve, la funzione è in C e il suo nome non dipende dal suo elenco di tipi di argomenti. Quindi il compilatore C ++ deve saperlo e contrassegnare quella funzione come C per evitare di fare il mangling.

Ricorda che detta funzione C potrebbe essere in una libreria il cui codice sorgente non hai, tutto ciò che hai è il binario precompilato e l'intestazione. Quindi il tuo compilatore C ++ non può fare "cose ​​sue", non può cambiare ciò che è nella libreria dopo tutto.


Questa è la parte che mi manca. Perché il compilatore C ++ dovrebbe manipolare un nome di funzione quando vede la sua dichiarazione o quando viene chiamata. Non vede solo i nomi delle funzioni mangle quando vede la loro implementazione? Questo avrebbe più senso per me
Engineer999

13
@ Engineer999: come si può avere un nome per la definizione e un altro per la dichiarazione? "C'è una funzione chiamata Brian che puoi chiamare." "Okay chiamerò Brian." "Spiacente, non esiste una funzione chiamata Brian." Si scopre che si chiama Graham.
Razze di leggerezza in orbita

Che dire del collegamento nelle librerie compilate in C ++? Quando il compilatore sta esaminando e compilando il nostro codice che chiama una delle funzioni in una libreria compilata C ++, come fa a sapere quale nome manipolare o dare alla funzione solo vedendo la sua dichiarazione o chiamata di funzione?
Engineer999,

1
@ Engineer999 Entrambi devono concordare sulla stessa manomissione. Quindi vedono il file header (ricordate, ci sono pochissimi metadati nelle DLL native - le intestazioni sono quei metadati), e vanno "Ah, giusto, Brian dovrebbe essere davvero Graham". Se questo non funziona (ad es. Con due schemi di manipolazione incompatibili), non otterrai un collegamento corretto e la tua applicazione fallirà. Il C ++ ha molte incompatibilità come questa. In pratica, devi usare esplicitamente il nome mangled e disabilitare il mangling dalla tua parte (es. Dici al tuo codice di eseguire Graham, non Brian). Nella attuale pratica ... extern "C":)
Luaan

1
@ Engineer999 Potrei sbagliarmi, ma forse hai esperienza con linguaggi come Visual Basic, C # o Java (o anche Pascal / Delphi in una certa misura)? Questi rendono l'interoperabilità estremamente semplice. In C e soprattutto in C ++, è tutt'altro. Ci sono molte convenzioni di chiamata che devi rispettare, devi sapere chi è responsabile di quale memoria e devi avere i file di intestazione che ti diano le dichiarazioni di funzione, dal momento che le stesse DLL non contengono abbastanza informazioni, specialmente nel caso di pure C. Se non si dispone di un file di intestazione, in genere è necessario decompilare la DLL per utilizzarlo.
Luaan,

32

cosa c'è di sbagliato nel consentire al compilatore C ++ di manipolare anche le funzioni C?

Non sarebbero più funzioni C.

Una funzione non è solo una firma e una definizione; come funziona una funzione è in gran parte determinato da fattori come la convenzione di chiamata. L '"interfaccia binaria dell'applicazione" specificata per l'uso sulla piattaforma descrive il modo in cui i sistemi dialogano tra loro. L'ABI C ++ utilizzata dal sistema specifica uno schema di modifica del nome, in modo che i programmi su quel sistema sappiano come richiamare le funzioni nelle librerie e così via. (Leggi l'ABI Itanium C ++ per un ottimo esempio. Vedrai molto rapidamente perché è necessario.)

Lo stesso vale per l'ABI C sul tuo sistema. Alcuni ABI C hanno effettivamente uno schema di modifica del nome (ad esempio Visual Studio), quindi si tratta meno di "disattivare la modifica del nome" e di più di passare dall'ABI C ++ all'ABI C, per alcune funzioni. Contrassegniamo le funzioni C come funzioni C, alle quali è pertinente l'ABI C (anziché l'ABI C ++). La dichiarazione deve corrispondere alla definizione (che si tratti dello stesso progetto o in una libreria di terze parti), altrimenti la dichiarazione non ha senso. Senza quello, il tuo sistema semplicemente non saprà come localizzare / invocare quelle funzioni.

Per quanto riguarda il motivo per cui le piattaforme non definiscono gli ABI C e C ++ come uguali e si liberano di questo "problema", questo è parzialmente storico - gli ABI C originali non erano sufficienti per C ++, che ha spazi dei nomi, classi e sovraccarico dell'operatore, tutto di cui è necessario in qualche modo essere rappresentati nel nome di un simbolo in un modo intuitivo, ma si potrebbe anche sostenere che far sì che i programmi C ora rispettino il C ++ sia ingiusto nei confronti della comunità C, che dovrebbe sopportare un modo molto più complicato ABI solo per il bene di altre persone che vogliono l'interoperabilità.


2
+int(PI/3), ma con un pizzico di sale: sarei molto cauto nel parlare di "C ++ ABI" ... AFAIK, ci sono tentativi di definire ABI C ++, ma nessun vero standard di fatto / de jure - come isocpp.org/files /papers/n4028.pdf afferma (e sono pienamente d'accordo), cito, è profondamente ironico che C ++ abbia in realtà sempre supportato un modo per pubblicare un'API con un ABI binario stabile — ricorrendo al sottoinsieme C di C ++ via extern “C ”. . C++ Itanium ABIè solo che - un po 'di C ++ ABI per Itanium ... come discusso in stackoverflow.com/questions/7492180/c-abi-issues-list

3
@vaxquis: Sì, non "ABI C ++", ma "ABI C ++" nello stesso modo in cui ho una "chiave di casa" che non funziona su tutte le case. Immagino che potrebbe essere più chiaro, anche se ho cercato di renderlo il più chiaro possibile iniziando con la frase "L'ABI C ++ in uso dal tuo sistema " . Ho lasciato cadere il chiaritore nelle successive dichiarazioni per brevità, ma accetterò una modifica che riduce la confusione qui!
Corse di leggerezza in orbita

1
AIUI C abi tendeva ad essere una proprietà di una piattaforma mentre C ++ ABI tendeva ad essere una proprietà di un singolo compilatore e spesso persino una proprietà di una singola versione di un compilatore. Quindi, se si desidera collegare tra moduli creati con strumenti di fornitori diversi, è necessario utilizzare un C abi per l'interfaccia.
lavaggio:

L'affermazione "Le funzioni alterate dal nome non sarebbero più funzioni C" è esagerata: è perfettamente possibile chiamare le funzioni alterate dal nome dalla semplice vaniglia C se il nome modificato è noto. Il fatto che il nome cambi non lo rende meno aderente all'ABI C, cioè non lo rende meno una funzione C. Al contrario, ha più senso: il codice C ++ non può chiamare una funzione C senza dichiararla "C" perché farebbe il nome mangling quando si tenta di collegarsi al chiamato.
Peter - Ripristina Monica il

@ PeterA.Schneider: Sì, la frase del titolo è esagerata. L' intero resto della risposta contiene i dettagli fattuali pertinenti.
Razze di leggerezza in orbita,

21

MSVC in effetti fa manipolare i nomi C, anche se in modo semplice. A volte aggiunge @4o un altro piccolo numero. Ciò riguarda le convenzioni di chiamata e la necessità di pulizia dello stack.

Quindi la premessa è solo imperfetta.


2
Questo non è davvero il nome che fa a pezzi. È semplicemente una convenzione di denominazione (o adornamento del nome) specifica del fornitore per evitare che problemi con gli eseguibili siano collegati a DLL create con le funzioni con convenzioni di chiamata diverse.
Peter,

2
Che ne dici di fare una prepagata con a _?
OrangeDog

12
@Peter: letteralmente la stessa cosa.
Razze di leggerezza in orbita

5
@Frankie_C: "Il chiamante pulisce lo stack" non è specificato da nessuno standard C: nessuna convenzione di chiamata è più standard dell'altra dal punto di vista del linguaggio.
Ben Voigt,

2
E dal punto di vista di MSVC, la "convenzione di chiamata standard" è proprio quello che scegli /Gd, /Gr, /Gv, /Gz. (Vale a dire, viene utilizzata la convenzione di chiamata standard a meno che una dichiarazione di funzione non specifichi esplicitamente una convenzione di chiamata.). Stai pensando a __cdeclquale sia la convenzione di chiamata standard predefinita.
Salterio

13

È molto comune avere programmi che sono in parte scritti in C e in parte scritti in un'altra lingua (spesso linguaggio assembly, ma a volte Pascal, FORTRAN o qualcos'altro). È anche comune avere programmi che contengono componenti diversi scritti da persone diverse che potrebbero non avere il codice sorgente per tutto.

Sulla maggior parte delle piattaforme esiste una specifica, spesso chiamata ABI [Application Binary Interface] che descrive cosa deve fare un compilatore per produrre una funzione con un nome particolare che accetta argomenti di alcuni tipi particolari e restituisce un valore di un determinato tipo. In alcuni casi, un ABI può definire più di una "convenzione di chiamata"; i compilatori per tali sistemi spesso forniscono un mezzo per indicare quale convenzione di chiamata dovrebbe essere usata per una particolare funzione. Ad esempio, su Macintosh, la maggior parte delle routine di Toolbox utilizza la convenzione di chiamata Pascal, quindi il prototipo di qualcosa come "LineTo" sarebbe qualcosa del tipo:

/* 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 tutto il codice in un progetto è stato compilato usando lo stesso compilatore, non importa quale nome il compilatore ha esportato per ciascuna funzione, ma in molte situazioni sarà necessario che il codice C chiami le funzioni che sono state compilate usando altri strumenti e non può essere ricompilato con il compilatore attuale [e potrebbe anche non essere nemmeno in C]. Essere in grado di definire il nome del linker è quindi fondamentale per l'uso di tali funzioni.


Sì, questa è la risposta. Se sono solo C e C ++, è difficile capire perché sia ​​fatto in questo modo. Per capire dobbiamo mettere le cose nel contesto del vecchio modo di collegare staticamente. Il collegamento statico sembra primitivo per i programmatori di Windows, ma è la ragione principale per cui C non riesce a manipolare i nomi.
user34660

2
@ user34660: non qutie. È la ragione per cui C non può imporre l'esistenza di caratteristiche la cui implementazione richiederebbe la manipolazione di nomi esportabili o la possibilità dell'esistenza di più simboli con nomi simili che si distinguono per caratteristiche secondarie.
supercat

sappiamo che ci sono stati tentativi di "imporre" tali cose o che tali estensioni erano disponibili per C prima di C ++?
user34660,

@ user34660: Ri "Il collegamento statico sembra primitivo per i programmatori di Windows ...", ma il collegamento dinamico a volte sembra una PITA importante per le persone che usano Linux, quando installare il programma X (probabilmente scritto in C ++) significa dover rintracciare e installare versioni particolari di librerie di cui hai già versioni diverse sul tuo sistema.
jamesqf,

@jamesqf, sì, Unix non aveva un collegamento dinamico prima di Windows. So molto poco sul collegamento dinamico in Unix / Linux ma sembra che non sia così fluido come potrebbe essere in un sistema operativo in generale.
user34660

12

Aggiungerò un'altra risposta, per affrontare alcune delle discussioni tangenziali che hanno avuto luogo.

L'ABI C (interfaccia binaria dell'applicazione) originariamente richiedeva il passaggio di argomenti sullo stack in ordine inverso (ovvero, spinto da destra a sinistra), dove il chiamante libera anche l'archiviazione dello stack. L'ABI moderno utilizza effettivamente i registri per passare argomenti, ma molte delle considerazioni mangling risalgono al passaggio dell'argomento stack originale.

L'ABI originale di Pascal, al contrario, ha spinto gli argomenti da sinistra a destra e la call ha dovuto far saltare gli argomenti. L'ABI C originale è superiore all'ABI originale Pascal in due punti importanti. L'ordine push dell'argomento indica che l'offset dello stack del primo argomento è sempre noto, consentendo funzioni che hanno un numero sconosciuto di argomenti, dove gli argomenti iniziali controllano quanti altri argomenti ci sono (ala printf).

Il secondo modo in cui l'ABI C è superiore è il comportamento nel caso in cui il chiamante e il chiamante non siano d'accordo su quanti argomenti ci siano. Nel caso C, fintanto che in realtà non accedi agli argomenti oltre l'ultimo, non succede nulla di brutto. In Pascal, il numero errato di argomenti viene estratto dallo stack e l'intero stack è danneggiato.

L'ABI originale di Windows 3.1 era basato su Pascal. Come tale, ha usato l'ABI di Pascal (argomenti nell'ordine da sinistra a destra, chiamate di chiamata). Poiché qualsiasi discrepanza nel numero dell'argomento potrebbe portare alla corruzione impilata, si è formato uno schema di demolizione. Ogni nome di funzione è stato modificato con un numero che indica la dimensione, in byte, dei suoi argomenti. Quindi, sulla macchina a 16 bit, la seguente funzione (sintassi C):

int function(int a)

È stato rovinato function@2, perché intè largo due byte. Ciò è stato fatto in modo che se la dichiarazione e la definizione non corrispondono, il linker non riuscirà a trovare la funzione anziché corrompere lo stack in fase di esecuzione. Al contrario, se il programma si collega, allora puoi essere sicuro che il numero corretto di byte sia estratto dallo stack alla fine della chiamata.

Windows a 32 bit e successivi utilizzano stdcallinvece l' ABI. È simile all'ABI di Pascal, tranne per il fatto che l'ordine di spinta è come in C, da destra a sinistra. Come l'ABI di Pascal, il nome mangling modifica le dimensioni del byte degli argomenti nel nome della funzione per evitare il danneggiamento dello stack.

A differenza delle affermazioni fatte altrove, la C ABI non altera i nomi delle funzioni, nemmeno su Visual Studio. Al contrario, le funzioni di manipolazione decorate con le stdcallspecifiche ABI non sono esclusive di VS. GCC supporta anche questa ABI, anche durante la compilazione per Linux. Questo è ampiamente utilizzato da Wine , che utilizza il proprio caricatore per consentire il collegamento in fase di esecuzione dei binari compilati di Linux alle DLL compilate di Windows.


9

I compilatori C ++ usano la modifica del nome per consentire nomi di simboli univoci per funzioni sovraccaricate la cui firma sarebbe altrimenti uguale. Fondamentalmente codifica anche i tipi di argomenti, il che consente il polimorfismo a livello di funzione.

C non lo richiede poiché non consente il sovraccarico delle funzioni.

Si noti che la modifica del nome è una (ma certamente non l'unica!) Ragione per cui non si può fare affidamento su un "ABI C ++".


8

Il C ++ vuole essere in grado di interagire con il codice C che collega contro di esso o contro cui si collega.

C si aspetta nomi di funzioni non alterati dal nome.

Se C ++ lo avesse alterato, non avrebbe trovato le funzioni non alterate esportate da C o C non avrebbe trovato le funzioni esportate da C ++. Il linker C deve ottenere il nome che si aspetta, perché non sa che proviene o va in C ++.


3

La modifica dei nomi delle funzioni e delle variabili C consentirebbe di verificarne i tipi al momento del collegamento. Attualmente, tutte le implementazioni (?) C consentono di definire una variabile in un file e chiamarla come funzione in un altro. Oppure puoi dichiarare una funzione con una firma sbagliata (es. void fopen(double)E poi chiamarla.

Ho proposto uno schema per il collegamento di tipo C delle variabili e delle funzioni C attraverso l'uso della manipolazione nel 1991. Lo schema non è mai stato adottato, perché, come altri hanno notato qui, ciò distruggerebbe la retrocompatibilità.


1
Intendi "consentire il controllo dei loro tipi al momento del collegamento ". I tipi vengono controllati in fase di compilazione, ma il collegamento con nomi non combinati non può verificare se le dichiarazioni utilizzate nelle diverse unità di compilazione sono d'accordo. E se non sono d'accordo, è il tuo sistema di build che è fondamentalmente rotto e deve essere riparato.
cmaster - reintegra monica il
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.