C ++: mancanza di standardizzazione a livello binario


14

Perché ISO / ANSI non ha standardizzato C ++ a livello binario? Ci sono molti problemi di portabilità con C ++, che è solo a causa della mancanza della sua standardizzazione a livello binario.

Scrive Don Box , (citando il suo libro Essential COM , capitolo COM As A Better C ++ )

C ++ e portabilità


Una volta presa la decisione di distribuire una classe C ++ come DLL, ci si trova di fronte a uno dei punti deboli fondamentali del C ++ , ovvero la mancanza di standardizzazione a livello binario . Sebbene il documento di lavoro sulla bozza ISO / ANSI C ++ tenti di codificare quali programmi verranno compilati e quali saranno gli effetti semantici della loro esecuzione, non tenta di standardizzare il modello di runtime binario di C ++. La prima volta che questo problema diventerà evidente è quando un client tenta di collegarsi alla libreria di importazione della DLL FastString da un ambiente di sviluppo C ++ diverso da quello utilizzato per creare la DLL FastString.

Ci sono più vantaggi o perdita di questa mancanza di standardizzazione binaria?


È meglio fare una domanda su programmers.stackexchange.com , visto che è più una domanda soggettiva?
Stephen Furlani,

1
Questione connessa mia realtà: stackoverflow.com/questions/2083060/...
Arak

4
Don Box è un fanatico. Ignoralo.
John Dibling,

8
Bene, C non è standardizzato nemmeno da ANSI / ISO a livello binario; OTOH C ha un de facto standard di ABI, piuttosto che una de jure uno. Il C ++ non ha un ABI così standardizzato perché diversi produttori avevano obiettivi diversi con le loro implementazioni. Ad esempio, eccezioni in VC ++ sulle spalle di Windows SEH. POSIX non ha SEH e quindi prendere quel modello non avrebbe avuto senso (quindi G ++ e MinGW non usano quel modello).
Billy ONeal,

3
Vedo questo come una caratteristica non una debolezza. Se associ un'implementazione a un ABI specifico, non avremo mai innovazione e il nuovo hardware sarà legato alla progettazione del linguaggio (e poiché ci sono 15 anni tra ogni nuova versione che è molto tempo nel settore hardware) e soffocando non verranno realizzate nuove idee per rendere più efficiente l'esecuzione del codice. Il prezzo è che tutto il codice in un eseguibile deve essere creato dallo stesso compilatore / versione (un problema ma non uno importante).

Risposte:


16

I linguaggi con un modulo compilato compatibile binario sono una fase relativamente nuova [*], ad esempio i tempi di esecuzione JVM e .NET. I compilatori C e C ++ solitamente emettono codice nativo.

Il vantaggio è che non è necessario un JIT, un interprete di bytecode, una macchina virtuale o qualsiasi altra cosa del genere. Ad esempio, non è possibile scrivere il codice bootstrap che viene eseguito all'avvio della macchina come bytecode Java piacevole e portatile, a meno che forse la macchina non possa eseguire nativamente il bytecode Java o non si abbia un qualche tipo di convertitore da Java a un nativo non binario compatibile codice eseguibile (in teoria: non sono sicuro che ciò possa essere raccomandato in pratica per il codice bootstrap). Potresti scriverlo in C ++, più o meno, anche se non in C ++ portatile anche a livello di sorgente, dal momento che farà molto casino con indirizzi hardware magici.

Lo svantaggio è che, naturalmente, il codice nativo viene eseguito solo sull'architettura per cui è stato compilato e gli eseguibili possono essere caricati solo da un caricatore che comprende il loro formato eseguibile e si collega e chiama solo in altri eseguibili per la stessa architettura e ABI.

Anche se arrivi così lontano, il collegamento di due eseguibili funzionerà correttamente solo fino a quando: (a) non violerai la One Definition Rule, che è facile da fare se sono stati compilati con compilatori / opzioni / qualunque altro, in modo tale che stessero usando definizioni diverse della stessa classe (o in un'intestazione o perché ciascuna di esse era staticamente collegata a implementazioni diverse); e (b) tutti i dettagli di implementazione rilevanti come il layout della struttura sono identici in base alle opzioni del compilatore in vigore al momento della compilazione di ciascuno.

Affinché lo standard C ++ definisca tutto ciò rimuoverà molte delle libertà attualmente disponibili per gli implementatori. Gli implementatori stanno usando queste libertà, specialmente quando scrivono codice di livello molto basso in C ++ (e C, che ha lo stesso problema).

Se vuoi scrivere qualcosa che assomigli un po 'a C ++, per un target binario-portatile, c'è C ++ / CLI, che si rivolge a .NET e Mono in modo da poter (si spera) eseguire .NET altrove rispetto a Windows. Penso che sia possibile persuadere il compilatore di MS a produrre assembly CIL puri che verranno eseguiti su Mono.

Ci sono anche potenzialmente cose che possono essere fatte con, ad esempio, LLVM per creare un ambiente C o C ++ portatile-binario. Non so però che sia emerso alcun esempio diffuso.

Ma tutto ciò si basa sulla correzione di molte cose che il C ++ rende dipendente dall'implementazione (come le dimensioni dei tipi). Quindi l'ambiente che comprende i binari portatili, deve essere disponibile sul sistema in cui deve essere eseguito il codice. Consentendo binari non portatili, C e C ++ possono andare in luoghi dove i binari portatili non possono, ed è per questo che lo standard non dice nulla sui binari.

Quindi su una determinata piattaforma, le implementazioni di solito ancora non forniscono compatibilità binaria tra diversi gruppi di opzioni, anche se lo standard non li si ferma. Se a Don Box non piace che i compilatori di Microsoft possano produrre binari incompatibili dalla stessa fonte, secondo le opzioni del compilatore, allora è il team di compilatore di cui deve lamentarsi. Il linguaggio C ++ non impedisce a un compilatore o un sistema operativo di fissare tutti i dettagli necessari, quindi una volta che ti limiti a Windows non è un problema fondamentale con C ++. Microsoft ha scelto di non farlo.

Le differenze spesso si manifestano come un'altra cosa che si può sbagliare e arrestare in modo anomalo il programma, ma potrebbero esserci notevoli vantaggi in termini di efficienza tra, ad esempio, versioni incompatibili di debug vs release di una dll.

[*] Non sono sicuro quando l'idea fu inventata per la prima volta, probabilmente nel 1642 o qualcosa del genere, ma la loro popolarità attuale è relativamente nuova, rispetto al tempo in cui C ++ si impegnò nelle decisioni di progettazione che le impedivano di definire la portabilità binaria.


@Steve Ma C ha un ABI ben definito su i386 e AMD64, quindi posso passare un puntatore a una funzione compilata da GCC versione X a una funzione compilata da MSVC versione Y. È impossibile farlo con una funzione C ++.
user877329

7

La compatibilità multipiattaforma e cross-compilatore non era l'obiettivo principale dietro C e C ++. Sono nati in un'epoca e pensavano a scopi per i quali le minimizzazioni specifiche di piattaforma e compilatore di tempo e spazio erano cruciali.

Da "Il design e l'evoluzione del C ++" di Stroustrup:

"L'obiettivo esplicito era quello di abbinare C in termini di tempo di esecuzione, compattezza del codice e compattezza dei dati. ... L'ideale - che è stato raggiunto - era che C con Classi potesse essere usato per qualunque C potesse essere usato."


1
+1 - esattamente. Come si potrebbe costruire un ABI standard che ha funzionato su entrambi i box ARM e Intel? Non avrebbe senso!
Billy ONeal,

1
sfortunatamente, non è riuscito in questo. Puoi fare tutto ciò che fa C ... tranne caricare dinamicamente un modulo C ++ in fase di esecuzione. devi "ripristinare" l'uso delle funzioni C nell'interfaccia esposta.
gbjbaanb,

6

Non è un bug, è una funzione! Ciò offre agli implementatori la libertà di ottimizzare la loro implementazione a livello binario. Il piccolo i386 e la sua progenie non sono le uniche CPU che hanno o esistono.


6

Il problema descritto nella citazione è causato dall'evitare deliberatamente la standardizzazione degli schemi di manipolazione dei nomi di simboli (penso che la " standardizzazione a livello binario " sia una frase fuorviante al riguardo, sebbene il problema sia correlato a quello di un compilatore binaria dell'applicazione ( ABI).

C ++ codifica le informazioni sulla firma e sul tipo di una funzione o di un oggetto dati e la sua appartenenza a classe / spazio dei nomi nel nome-simbolo e ai diversi compilatori è consentito utilizzare schemi diversi. Di conseguenza, un simbolo in una libreria statica, una DLL o un file oggetto non si collegherà al codice compilato utilizzando un compilatore diverso (o eventualmente anche una versione diversa dello stesso compilatore).

Il problema è descritto e spiegato probabilmente meglio di quanto posso qui , con esempi di schemi usati da diversi compilatori.

Le ragioni per la deliberata mancanza di standardizzazione sono spiegati qui .


3

Lo scopo di ISO / ANSI era di standardizzare il linguaggio C ++, problema che sembra essere abbastanza complesso da richiedere anni per avere un aggiornamento degli standard linguistici e il supporto del compilatore.

La compatibilità binaria è molto più complessa, dato che i binari devono essere eseguiti su architetture di CPU diverse e ambienti operativi diversi.


È vero, ma il problema descritto nella citazione non è in realtà nulla a che fare con la "compatibilità a livello binario" (nonostante l'uso del termine da parte dell'autore) in alcun senso diverso da tali cose sono definite in qualcosa chiamato "Application Binary Interface". Sta infatti descrivendo la questione degli schemi incompatibili di manipolazione dei nomi.

@Clifford: lo schema di manipolazione dei nomi è solo un sottoinsieme della compatibilità a livello binario. quest'ultimo è più simile a un termine generico!
Nawaz,

Dubito che ci sia un problema con il tentativo di eseguire un binario Linux su una macchina Windows. Le cose sarebbero molto meglio se esistesse una ABI per piattaforma, poiché almeno un linguaggio di script potrebbe caricare ed eseguire dinamicamente un binario sulla stessa piattaforma o se le app potrebbero utilizzare componenti creati con un compilatore diverso. Oggi non puoi usare una Cll su Linux, e nessuno si lamenta, ma quella Cll può ancora essere caricata da un'app Python che è dove il vantaggio aumenta.
gbjbaanb,

2

Come diceva Andy, la compatibilità multipiattaforma non era un grande obiettivo, mentre l'implementazione di un'ampia piattaforma e hardware era un obiettivo, con il risultato netto che è possibile scrivere implementazioni conformi per una vasta gamma di sistemi. La standardizzazione binaria lo avrebbe reso praticamente irrealizzabile.

Anche la compatibilità con C era importante e ciò l'avrebbe notevolmente complicato.

Successivamente sono stati compiuti alcuni sforzi per standardizzare l'ABI per un sottoinsieme di implementazioni.


Accidenti, ho dimenticato la compatibilità C. Buon punto, +1!
Andy Thomas,

1

Penso che la mancanza di uno standard per il C ++ sia un problema nel mondo odierno della programmazione modulare disaccoppiata. Tuttavia, dobbiamo definire ciò che vogliamo da tale standard.

Nessuno nella loro mente giusta vuole definire l'implementazione o la piattaforma per un binario. Quindi non puoi prendere una dll di Windows x86 e iniziare a usarla su una piattaforma Linux x86_64. Sarebbe un po 'troppo.

Tuttavia, ciò che la gente vuole è la stessa cosa che abbiamo con i moduli C: un'interfaccia standardizzata a livello binario (cioè una volta compilata). Attualmente, se si desidera caricare una dll in un'app modulare, si esportano le funzioni C e si associano ad esse in fase di esecuzione. Non puoi farlo con un modulo C ++. Sarebbe bello se potessi, il che significherebbe anche che le DLL scritte con un compilatore potrebbero essere caricate da un altro. Certo, non saresti ancora in grado di caricare una DLL creata per una piattaforma incompatibile, ma non è un problema che deve essere risolto.

Quindi se il corpo degli standard definisse quale interfaccia ha esposto un modulo, allora avremmo molta più flessibilità nel caricamento dei moduli C ++, non dovremmo esporre il codice C ++ come codice C e probabilmente avremmo molto più uso di C ++ nei linguaggi di script.

Inoltre non dovremmo soffrire cose come COM che tentano di fornire una soluzione a questo problema.


1
+1. Sì sono d'accordo. Le altre risposte qui sostanzialmente eliminano il problema dicendo che la standardizzazione binaria proibirebbe le ottimizzazioni specifiche dell'architettura. Ma non è questo il punto. Nessuno sta sostenendo un formato eseguibile binario multipiattaforma. Il problema è che non esiste un'interfaccia standard per caricare i moduli C ++ in modo dinamico.
Charles Salvia,

1

Ci sono molti problemi di portabilità con C ++, che è solo a causa della mancanza della sua standardizzazione a livello binario.

Non penso sia così semplice. Le risposte fornite forniscono già un'ottima motivazione sulla mancanza di attenzione alla standardizzazione, ma il C ++ potrebbe essere troppo ricco di un linguaggio per essere adatto per competere sinceramente con il C come standard ABI.

Siamo in grado di ingannare il nome derivante da sovraccarico di funzioni, incompatibilità vtable, incompatibilità con eccezioni oltre i limiti del modulo, ecc. Tutti questi sono un vero dolore, e vorrei che potessero almeno standardizzare i layout di vtable.

Ma uno standard ABI non riguarda solo la produzione di dylibs C ++ prodotti in un compilatore in grado di essere utilizzati da un altro binario creato da un altro compilatore. L'ABI è utilizzato in più lingue . Sarebbe bello se almeno potessero coprire la prima parte, ma non vedo in alcun modo C ++ competere veramente con C nel tipo di ABI universale così cruciale per realizzare i dylibs più ampiamente compatibili.

Immagina una semplice coppia di funzioni esportate in questo modo:

void f(Foo foo);
void f(Bar bar, int val);

... e immaginare Fooe Barsono classi con costruttori con parametri, copiare costruttori, costruttori spostare, e distruttori non banali.

Quindi prendi lo scenario di un Python / Lua / C # / Java / Haskell / etc. lo sviluppatore sta cercando di importare questo modulo e utilizzarlo nella loro lingua.

Per prima cosa avremmo bisogno di uno standard di modifica del nome su come esportare i simboli utilizzando il sovraccarico delle funzioni. Questa è una parte più semplice Eppure non dovrebbe davvero essere chiamato "mangling". Poiché gli utenti del dylib devono cercare i simboli per nome, i sovraccarichi qui dovrebbero portare a nomi che non sembrano un disastro completo. Forse i nomi dei simboli potrebbero essere simili "f_Foo" "f_Bar_int"o qualcosa del genere. Dovremmo essere sicuri che non possano scontrarsi con un nome effettivamente definito dallo sviluppatore, magari riservando alcuni simboli / caratteri / convenzioni per l'utilizzo dell'ABI.

Ma ora uno scenario più difficile. In che modo lo sviluppatore Python, ad esempio, invoca costruttori di spostamento, copia costruttori e distruttori? Forse potremmo esportarli come parte del dylib. Ma cosa succede se Fooe Barvengono esportati in diversi moduli? Dobbiamo duplicare i simboli e le implementazioni associati a questo dylib o no? Suggerirei di farlo, dal momento che potrebbe diventare davvero fastidioso molto velocemente, altrimenti iniziare a doversi impigliare in più interfacce dylib solo per creare un oggetto qui, passarlo qui, copiarne uno lì, distruggerlo qui. Mentre la stessa preoccupazione di base potrebbe applicarsi in qualche modo in C (solo più manualmente / esplicitamente), C tende ad evitarlo solo per natura del modo in cui le persone programmano con esso.

Questo è solo un piccolo esempio dell'imbarazzo. Cosa succede quando una delle ffunzioni sopra lancia una BazException(anche una classe C ++ con costruttori e distruttori e deriva std :: exception) in JavaScript?

Nella migliore delle ipotesi, possiamo solo sperare di standardizzare un ABI che funziona da un binario prodotto da un compilatore C ++ a un altro binario prodotto da un altro. Sarebbe fantastico, ovviamente, ma volevo solo sottolineare questo. Accompagnare in genere tali preoccupazioni per la distribuzione di una libreria generalizzata che funziona tra compilatori è spesso anche il desiderio di renderla veramente generalizzata e compatibile con più lingue.

Soluzione suggerita

La mia soluzione suggerita dopo aver faticato a trovare modi per utilizzare le interfacce C ++ per API / ABI per anni con interfacce in stile COM è quella di diventare uno sviluppatore "C / C ++" (gioco di parole).

Usa C per creare quegli ABI universali, con C ++ per l'implementazione. Possiamo ancora fare cose come le funzioni di esportazione che restituiscono i puntatori a classi C ++ opache con funzioni esplicite per creare e distruggere tali oggetti nell'heap. Cerca di innamorarti dell'estetica C dal punto di vista ABI anche se utilizziamo totalmente C ++ per l'implementazione. Le interfacce astratte possono essere modellate utilizzando tabelle di puntatori a funzioni. È noioso racchiudere questa roba in un'API C, ma i vantaggi e la compatibilità della distribuzione che ne deriva tenderanno a renderlo molto utile.

Quindi, se non ci piace usare questa interfaccia così direttamente (probabilmente non dovremmo almeno per ragioni RAII), possiamo racchiuderlo in una libreria C ++ collegata staticamente fornita con l'SDK. I client C ++ possono usarlo.

I client Python non vorranno usare direttamente un'interfaccia C o C ++ in quanto non c'è modo di renderli pythonique. Avranno voglia di avvolgerlo nelle proprie interfacce Pythonique, quindi in realtà è una buona cosa che stiamo solo esportando un minimo C API / ABI per renderlo il più semplice possibile.

Penso che gran parte del settore C ++ trarrebbe beneficio dal fare questo piuttosto che provare a spedire ostinatamente interfacce in stile COM e così via. Inoltre, renderebbe più facile la vita di tutti gli utenti di questi dylibs per non doversi preoccupare di ABI scomodi. C lo rende semplice e la sua semplicità dal punto di vista ABI ci consente di creare API / ABI che funzionano in modo naturale e con minimalismo per tutti i tipi di SFI.


1
"Usa C per creare quegli ABI universali, con C ++ per l'implementazione." ... faccio lo stesso, come molti altri!
Nawaz,

-1

Non so perché non si standardizzi a livello binario. Ma so cosa faccio al riguardo. Su Windows dichiaro la funzione extern "C" BOOL WINAPI. (Naturalmente sostituisci BOOL con qualunque tipo di funzione.) E vengono esportati in modo pulito.


2
Ma se lo dichiari extern "C", utilizzerà l'ABI C, che di fatto è uno standard sull'hardware comune del PC, anche se non è imposto da alcun tipo di comitato.
Billy ONeal,

-3

Utilizzare unzip foo.zip && make foo.exe && foo.exese si desidera la portabilità della propria fonte.

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.