C ++: Fehlende Standardisierung auf Binärebene

14

Warum hat ISO / ANSI C ++ nicht auf binärer Ebene standardisiert? Es gibt viele Portabilitätsprobleme mit C ++, die nur auf die fehlende Standardisierung auf Binärebene zurückzuführen sind.

Don Box schreibt, (aus seinem Buch Essential COM , Kapitel COM As A Better C ++ zitierend )

C ++ und Portabilität


Sobald die Entscheidung getroffen ist eine C ++ Klasse als DLL zu verteilen, ein mit einem konfrontiert die grundlegenden Schwächen der C ++ , das heißt, fehlende Standardisierung auf der binären Ebene . Obwohl das ISO / ANSI C ++ Draft Working Paper versucht zu kodifizieren, welche Programme kompiliert werden und welche semantischen Auswirkungen ihre Ausführung haben wird, wird nicht versucht, das binäre Laufzeitmodell von C ++ zu standardisieren. Dieses Problem tritt zum ersten Mal auf, wenn ein Client versucht, eine Verknüpfung mit der Importbibliothek der FastString-DLL aus einer anderen C ++ - Entwicklungsumgebung als der zum Erstellen der FastString-DLL verwendeten herzustellen.

Gibt es weitere Vorteile oder den Verlust dieser fehlenden binären Standardisierung?

Nawaz
quelle
Wird dies auf programmers.stackexchange.com besser gestellt , da es sich eher um eine subjektive Frage handelt?
Stephen Furlani
1
Verwandte Frage von mir tatsächlich: stackoverflow.com/questions/2083060/…
AraK
4
Don Box ist ein Eiferer. Ignoriere ihn.
John Dibling
8
Nun, C ist auch in der Binär-Ebene nicht von ANSI / ISO standardisiert. OTOH C hat einen de-facto- Standard-ABI und keinen de-jure- Standard . C ++ hat kein derart standardisiertes ABI, da verschiedene Hersteller unterschiedliche Ziele bei ihren Implementierungen hatten. Zum Beispiel Ausnahmen in VC ++ Piggyback auf Windows SEH. POSIX hat kein SEH und daher hätte es keinen Sinn gemacht, dieses Modell zu nehmen (G ++ und MinGW verwenden dieses Modell also nicht).
Billy ONeal
3
Ich sehe dies als Merkmal und nicht als Schwäche. Wenn Sie eine Implementierung an ein bestimmtes ABI binden, werden wir niemals Innovationen haben und neue Hardware wird an das Design der Sprache gebunden sein (und da zwischen jeder neuen Version, die in der Hardware-Branche lange Zeit existiert, 15 Jahre liegen) und ersticken Innovative neue Ideen, mit denen der Code effizienter ausgeführt werden soll, werden nicht umgesetzt. Der Preis ist, dass der gesamte Code in einer ausführbaren Datei von demselben Compiler / derselben Version erstellt werden muss (ein Problem, aber kein schwerwiegendes).

Antworten:

16

Sprachen mit binärkompatibler kompilierter Form sind eine relativ neue Phase [*], zum Beispiel die JVM- und .NET-Laufzeit. C- und C ++ - Compiler geben normalerweise systemeigenen Code aus.

Der Vorteil ist, dass keine JIT, kein Bytecode-Interpreter, keine VM oder ähnliches erforderlich ist. Beispielsweise können Sie den Bootstrap-Code, der beim Systemstart ausgeführt wird, nicht als netten, portablen Java-Bytecode schreiben, es sei denn, der Computer kann Java-Bytecode nativ ausführen, oder Sie haben eine Art Konverter von Java in einen nicht-binär-kompatiblen nativen Code ausführbarer Code (theoretisch: nicht sicher, ob dies in der Praxis für Bootstrap-Code empfohlen werden kann). Sie könnten es mehr oder weniger in C ++ schreiben, wenn auch nicht in portablem C ++, selbst auf der Source-Ebene, da es viel mit magischen Hardware-Adressen zu tun hat.

Der Nachteil ist, dass nativer Code natürlich nur auf der Architektur ausgeführt wird, für die er kompiliert wurde, und die ausführbaren Dateien nur von einem Loader geladen werden können, der ihr ausführbares Format versteht, und nur mit anderen ausführbaren Dateien für dieselbe Architektur verknüpft und diese aufgerufen werden können und ABI.

Selbst wenn Sie so weit kommen, funktioniert das Verknüpfen zweier ausführbarer Dateien nur dann richtig, wenn: (a) Sie nicht gegen die Ein-Definition-Regel verstoßen. Dies ist einfach, wenn sie mit verschiedenen Compilern / Optionen / was auch immer kompiliert wurden. so, dass sie unterschiedliche Definitionen derselben Klasse verwendeten (entweder in einem Header oder weil sie jeweils statisch mit unterschiedlichen Implementierungen verknüpft waren); und (b) alle relevanten Implementierungsdetails, wie z. B. das Strukturlayout, sind gemäß den Compileroptionen, die bei der Kompilierung jeweils gültig waren, identisch.

Wenn der C ++ - Standard all dies definiert, würden viele der derzeit für Implementierer verfügbaren Freiheiten verloren gehen. Implementierer nutzen diese Freiheiten, insbesondere beim Schreiben von Code auf sehr niedriger Ebene in C ++ (und C, das das gleiche Problem aufweist).

Wenn Sie etwas schreiben möchten, das ein bisschen wie C ++ aussieht, gibt es für ein binär portierbares Ziel C ++ / CLI, das auf .NET und Mono abzielt, so dass Sie .NET (hoffentlich) anders als unter Windows ausführen können. Ich denke, es ist möglich, den MS-Compiler davon zu überzeugen, reine CIL-Assemblys zu erstellen, die unter Mono ausgeführt werden können.

Möglicherweise können auch Dinge mit LLVM ausgeführt werden, um eine binär portable C- oder C ++ - Umgebung zu erstellen. Ich weiß jedoch nicht, dass sich ein weit verbreitetes Beispiel herauskristallisiert hat.

Dies alles hängt jedoch davon ab, dass viele Dinge behoben werden, die von der Implementierung in C ++ abhängig sind (z. B. die Größe der Typen). Dann muss die Umgebung, die die tragbaren Binärdateien versteht, auf dem System verfügbar sein, auf dem der Code ausgeführt werden soll. Durch das Zulassen nicht portierbarer Binärdateien können C und C ++ an Stellen eingesetzt werden, an denen tragbare Binärdateien nicht möglich sind, und aus diesem Grund sagt der Standard überhaupt nichts über Binärdateien aus.

Dann auf einer bestimmten Plattform - Implementierungen in der Regel noch nicht bieten binäre Kompatibilität zwischen den verschiedenen Gruppen von Optionen, obwohl der Standard nicht , sie zu stoppen ist. Wenn es Don Box nicht gefällt, dass Microsofts Compiler inkompatible Binärdateien aus derselben Quelle produzieren können, ist es das Compilerteam, über das er sich beschweren muss. Die Sprache C ++ nicht verbieten einen Compiler oder ein Betriebssystem von Fesselung alle notwendigen Details, so , wenn Sie sich auf Windows beschränken , es ist kein grundsätzliches Problem mit C ++. Microsoft hat sich entschieden, dies nicht zu tun.

Die Unterschiede manifestieren sich oft als eine weitere Sache, die Sie falsch machen und Ihr Programm zum Absturz bringen können, aber es kann beträchtliche Effizienzgewinne geben, beispielsweise zwischen inkompatiblem Debug und Release-Versionen einer DLL.

[*] Ich bin mir nicht sicher, wann die Idee zum ersten Mal erfunden wurde, wahrscheinlich 1642 oder so, aber ihre aktuelle Beliebtheit ist relativ neu im Vergleich zu der Zeit, als C ++ sich auf die Entwurfsentscheidungen festgelegt hat, die verhindern, dass sie die Binärportabilität definieren.

Steve Jessop
quelle
@Steve Aber C hat ein gut definiertes ABI auf i386 und AMD64, so dass ich einen Zeiger auf eine von GCC Version X kompilierte Funktion an eine von MSVC Version Y kompilierte Funktion übergeben kann. Das mit einer C ++ - Funktion zu tun ist unmöglich.
user877329
7

Plattformübergreifende und compilerübergreifende Kompatibilität waren nicht die Hauptziele von C und C ++. Sie wurden in einer Ära geboren und für Zwecke gedacht, für die plattformspezifische und compilerspezifische Minimierungen von Zeit und Raum entscheidend waren.

Aus Stroustrups "Das Design und die Entwicklung von C ++":

"Das ausdrückliche Ziel bestand darin, C in Bezug auf Laufzeit, Code-Kompaktheit und Daten-Kompaktheit gleichzusetzen. ... Das Ideal, das erreicht wurde, war, dass C mit Klassen für alles verwendet werden konnte, wofür C verwendet werden konnte."

Andy Thomas
quelle
1
+1 - genau. Wie würde man ein Standard-ABI bauen, das sowohl auf ARM- als auch auf Intel-Boxen funktioniert? Würde keinen Sinn ergeben!
Billy ONeal
1
leider ist es dabei gescheitert. Sie können alles tun, was C tut, außer ein C ++ - Modul zur Laufzeit dynamisch zu laden. Sie müssen auf die Verwendung von C-Funktionen in der exponierten Schnittstelle zurückgreifen.
Gbjbaanb
6

Es ist kein Fehler, es ist ein Feature! Dies gibt Implementierern die Freiheit, ihre Implementierung auf binärer Ebene zu optimieren. Der Little-Endian i386 und seine Nachkommen sind nicht die einzigen CPUs, die es gibt oder gibt.


quelle
6

Das Problem im Angebote beschrieben wird durch die ganz bewusste Vermeidung von Standardisierung von Symbol-Namen Mangeln Systemen verursacht (ich glaube , „ Standardisierung auf der binären Ebene “ ist ein irreführender Begriff in diesem Zusammenhang , obwohl die Ausgabe zu einem Compiler verwandt Application Binary Interface ( ABI).

C ++ codiert die Signatur- und Typinformationen einer Funktion oder eines Datenobjekts sowie deren Klassen- / Namespacemitgliedschaft in den Symbolnamen. Verschiedene Compiler dürfen unterschiedliche Schemata verwenden. Folglich wird ein Symbol in einer statischen Bibliothek, DLL oder Objektdatei nicht mit Code verknüpft, der mit einem anderen Compiler (oder möglicherweise sogar einer anderen Version desselben Compilers) kompiliert wurde.

Das Problem wird wahrscheinlich besser beschrieben und erklärt als ich es hier kann , mit Beispielen für Schemata, die von verschiedenen Compilern verwendet werden.

Die Gründe für den absichtlichen Mangel an Standardisierung sind auch erklärt hier .

Clifford
quelle
3

Das Ziel von ISO / ANSI war die Standardisierung der C ++ - Sprache, ein Problem, das so komplex zu sein scheint, dass Jahre erforderlich sind, um die Sprachstandards und die Compilerunterstützung zu aktualisieren.

Die Binärkompatibilität ist viel komplexer, da die Binärdateien auf verschiedenen CPU-Architekturen und verschiedenen Betriebssystemumgebungen ausgeführt werden müssen.


quelle
Es ist wahr, aber das im Zitat beschriebene Problem hat in der Tat nichts mit "Kompatibilität auf Binärebene" zu tun (trotz der Verwendung des Begriffs durch den Autor), außer solche Dinge, die als "Application Binary Interface" bezeichnet werden. Tatsächlich beschreibt er das Problem inkompatibler Namensverwaltungsschemata.
@ Clifford: Das Namensverwaltungsschema ist nur eine Teilmenge der Kompatibilität auf Binärebene. Letzteres ist eher ein Überbegriff!
Nawaz,
Ich bezweifle, dass es ein Problem mit dem Versuch gibt, eine Linux-Binärdatei auf einem Windows-Computer auszuführen. Es wäre viel besser, wenn es eine ABI pro Plattform gäbe, da zumindest dann eine Skriptsprache dynamisch eine Binärdatei auf derselben Plattform laden und ausführen könnte oder Apps Komponenten verwenden könnten, die mit einem anderen Compiler erstellt wurden. Sie können heute keine C-DLL unter Linux verwenden, und niemand beschwert sich darüber, aber diese C-DLL kann immer noch von einer Python-App geladen werden, von der der Vorteil ausgeht.
Gbjbaanb
2

Wie Andy sagte, war die plattformübergreifende Kompatibilität kein großes Ziel, wohingegen eine breite Plattform- und Hardwareimplementierung ein Ziel war, mit dem Ergebnis, dass Sie konforme Implementierungen für eine sehr große Auswahl von Systemen schreiben können. Eine binäre Standardisierung hätte dies praktisch unmöglich gemacht.

Die C-Kompatibilität war ebenfalls wichtig und hätte dies erheblich erschwert.

In der Folge wurden einige Anstrengungen unternommen, um die ABI für eine Untergruppe von Implementierungen zu standardisieren .

Flexo
quelle
Verdammt, ich habe die C-Kompatibilität vergessen. Guter Punkt, +1!
Andy Thomas
1

Ich denke, das Fehlen eines Standards für C ++ ist ein Problem in der heutigen Welt der entkoppelten, modularen Programmierung. Wir müssen jedoch definieren, was wir von einem solchen Standard wollen.

Niemand, der bei klarem Verstand ist, möchte die Implementierung oder Plattform für eine Binärdatei definieren. Sie können eine x86-Windows-DLL also nicht auf einer x86_64-Linux-Plattform verwenden. Das wäre ein bisschen viel.

Was die Leute jedoch wollen, ist dasselbe, was wir mit C-Modulen haben - eine standardisierte Schnittstelle auf Binärebene (dh einmal kompiliert). Wenn Sie derzeit eine DLL in eine modulare App laden möchten, exportieren Sie C-Funktionen und binden sie zur Laufzeit an diese. Mit einem C ++ - Modul ist das nicht möglich. Es wäre großartig, wenn Sie könnten, was auch bedeuten würde, dass mit einem Compiler geschriebene DLLs von einem anderen geladen werden könnten. Sicher, Sie wären immer noch nicht in der Lage, eine für eine inkompatible Plattform erstellte DLL zu laden, aber das ist kein Problem, das behoben werden muss.

Wenn also der Standardkörper definiert, welche Schnittstelle ein Modul verfügbar macht, haben wir viel mehr Flexibilität beim Laden von C ++ - Modulen. Wir müssten C ++ - Code nicht als C-Code verfügbar machen, und wir würden wahrscheinlich viel mehr davon nutzen von C ++ in Skriptsprachen.

Wir müssten auch Dinge wie COM nicht leiden, die versuchen, eine Lösung für dieses Problem zu finden.

gbjbaanb
quelle
1
+1. Ja ich stimme zu. Die anderen Antworten hier haben das Problem im Grunde genommen dadurch gelöst, dass die binäre Standardisierung architekturspezifische Optimierungen verbietet. Darum geht es aber nicht. Niemand spricht sich für ein plattformübergreifendes ausführbares Binärformat aus. Das Problem ist , dass es keine Standard - Schnittstelle dynamisch C ++ Module zu laden.
Charles Salvia
1

Es gibt viele Portabilitätsprobleme mit C ++, die nur auf die fehlende Standardisierung auf Binärebene zurückzuführen sind.

Ich denke nicht, dass es so einfach ist. Die Antworten liefern bereits hervorragende Gründe für die mangelnde Konzentration auf die Standardisierung, aber C ++ ist möglicherweise zu sprachenreich, um wirklich mit C als ABI-Standard konkurrieren zu können.

Wir können auf die Namensverfälschung eingehen, die sich aus Funktionsüberladung, Inkompatibilitäten von V-Tabellen, Inkompatibilitäten mit Ausnahmen über Modulgrenzen hinweg usw. ergibt. All dies ist ein echtes Problem, und ich wünschte, sie könnten zumindest die Layouts von V-Tabellen standardisieren.

Bei einem ABI-Standard geht es jedoch nicht nur darum, C ++ - Dylibs, die in einem Compiler erstellt wurden, so zu machen, dass sie von einer anderen Binärdatei verwendet werden können, die von einem anderen Compiler erstellt wurde. ABI wird sprachenübergreifend verwendet . Es wäre schön, wenn sie zumindest den ersten Teil abdecken könnten, aber ich sehe keine Möglichkeit, dass C ++ jemals wirklich mit C auf einer universellen ABI-Ebene konkurriert, die für die Herstellung der am weitesten kompatiblen Dylibs so wichtig ist.

Stellen Sie sich ein einfaches Funktionspaar vor, das wie folgt exportiert wird:

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

... und sich vorstellen , Foound Barwaren Klassen mit parametrisierte Konstrukteure, Kopierkonstruktoren, verschieben Bauer, und nicht-triviale Destruktoren.

Nehmen Sie dann das Szenario eines Python / Lua / C # / Java / Haskell / etc. Entwickler, der versucht, dieses Modul zu importieren und in seiner Sprache zu verwenden.

Zuerst benötigen wir einen Namensverwaltungsstandard für den Export von Symbolen unter Verwendung von Funktionsüberladung. Dies ist ein einfacher Teil. Dabei sollte es eigentlich nicht "Mangeln" heißen. Da Benutzer der Dylib Symbole nach Namen suchen müssen, sollten die Überladungen hier zu Namen führen, die nicht wie ein vollständiges Durcheinander aussehen. Vielleicht könnten die Symbolnamen so "f_Foo" "f_Bar_int"oder so ähnlich sein . Wir müssten sicherstellen, dass sie nicht mit einem vom Entwickler definierten Namen in Konflikt geraten können, und möglicherweise einige Symbole / Zeichen / Konventionen für die ABI-Verwendung reservieren.

Aber jetzt ein schwierigeres Szenario. Wie ruft der Python-Entwickler beispielsweise Move-Konstruktoren, Copy-Konstruktoren und Destruktoren auf? Vielleicht könnten wir diese als Teil der Dylib exportieren. Was aber, wenn Foound Barin verschiedenen Modulen exportiert werden? Sollten wir die in dieser Dylib enthaltenen Symbole und Implementierungen duplizieren oder nicht? Ich würde vorschlagen, dass wir dies tun, da es sonst sehr schnell nervig werden könnte, sich in mehreren Dylib-Schnittstellen zu verfangen, nur um hier ein Objekt zu erstellen, es hier zu übergeben, eines dort zu kopieren, es hier zu zerstören. Während das gleiche grundlegende Anliegen in C (nur manuell / explizit) etwas zutreffen könnte, tendiert C dazu, dies aufgrund der Art und Weise, wie die Leute damit programmieren, zu vermeiden.

Dies ist nur ein kleines Beispiel für die Unbeholfenheit. Was passiert, wenn eine der foben genannten Funktionen eine BazException(auch eine C ++ - Klasse mit Konstruktoren und Destruktoren und Ableitung der std :: exception) in JavaScript wirft ?

Ich denke, wir können bestenfalls hoffen, ein ABI zu standardisieren, das von einer Binärdatei, die von einem C ++ - Compiler erzeugt wird, zu einer anderen Binärdatei, die von einem anderen erzeugt wird, funktioniert. Das wäre natürlich großartig, aber ich wollte nur darauf hinweisen. In der Regel geht mit solchen Überlegungen zur Verteilung einer generalisierten Bibliothek, die compilerübergreifend funktioniert, auch der Wunsch einher, eine wirklich generalisierte und kompatible sprachübergreifende Bibliothek zu erstellen.

Vorgeschlagene Lösung

Meine vorgeschlagene Lösung, nachdem ich jahrelang Mühe hatte, Möglichkeiten zu finden, C ++ - Schnittstellen für APIs / ABIs mit COM-ähnlichen Schnittstellen zu verwenden, besteht darin, einfach ein "C / C ++" - (Wortspiel-) Entwickler zu werden.

Verwenden Sie C, um diese universellen ABIs zu erstellen, mit C ++ für die Implementierung. Wir können weiterhin Dinge wie Exportfunktionen ausführen, die Zeiger auf undurchsichtige C ++ - Klassen mit expliziten Funktionen zurückgeben, um solche Objekte auf dem Heap zu erstellen und zu zerstören. Versuchen Sie, sich aus ABI-Sicht in diese C-Ästhetik zu verlieben, auch wenn wir C ++ für die Implementierung verwenden. Abstrakte Schnittstellen können mit Tabellen von Funktionszeigern modelliert werden. Es ist mühsam, dieses Zeug in eine C-API zu packen, aber die damit verbundenen Vorteile und die Kompatibilität der Distribution werden es in der Regel sehr lohnenswert machen.

Wenn wir diese Schnittstelle dann nicht so gerne direkt verwenden (wir sollten dies wahrscheinlich nicht aus RAII-Gründen tun), können wir alles, was wir wollen, in eine statisch verknüpfte C ++ - Bibliothek packen, die wir mit dem SDK ausliefern. C ++ Clients können das nutzen.

Python-Clients möchten weder eine C- noch eine C ++ - Schnittstelle direkt verwenden, da es keine Möglichkeit gibt, diese Pythonique zu erstellen. Sie werden es in ihre eigenen Pythonique-Interfaces packen wollen, also ist es eigentlich eine gute Sache, dass wir nur ein Minimum an C API / ABI exportieren, um das so einfach wie möglich zu machen.

Ich denke, ein Großteil der C ++ - Industrie würde davon mehr profitieren, als hartnäckig zu versuchen, Schnittstellen im COM-Stil und so weiter zu liefern. Es würde uns als Benutzer dieser Dylibs auch das ganze Leben leichter machen, uns nicht mit umständlichen ABIs herumschlagen zu müssen. C macht es einfach, und die Einfachheit aus ABI-Sicht ermöglicht es uns, APIs / ABIs zu erstellen, die für alle Arten von FFIs natürlich und minimalistisch funktionieren.


quelle
1
"Verwenden Sie C, um diese universellen ABIs zu erstellen, mit C ++ für die Implementierung." ... ich mache das gleiche, wie viele andere!
Nawaz
-1

Ich weiß nicht, warum es nicht auf binärer Ebene standardisiert wird. Aber ich weiß, was ich dagegen mache. Unter Windows deklariere ich die Funktion extern "C" BOOL WINAPI. (Ersetzen Sie BOOL natürlich durch den Typ der Funktion.) Und sie werden sauber exportiert.

Mike Jones
quelle
2
Wenn Sie es jedoch deklarieren extern "C", wird das C ABI verwendet, ein De-facto- Standard für gängige PC-Hardware, auch wenn es von keinem Ausschuss vorgeschrieben wird.
Billy ONeal
-3

Verwenden unzip foo.zip && make foo.exe && foo.exeSie diese Option, wenn Sie die Portabilität Ihrer Quelle wünschen.

Sjoerd
quelle