C ++: Manque de standardisation au niveau binaire


14

Pourquoi ISO / ANSI n'a pas standardisé C ++ au niveau binaire? Il y a beaucoup de problèmes de portabilité avec C ++, ce qui est uniquement dû au manque de standardisation au niveau binaire.

Don Box écrit, (citant son livre Essential COM , chapitre COM As A Better C ++ )

C ++ et portabilité


Une fois la décision prise de distribuer une classe C ++ en tant que DLL, on est confronté à l'une des faiblesses fondamentales de C ++ , à savoir le manque de standardisation au niveau binaire . Bien que le projet de document de travail ISO / ANSI C ++ tente de codifier les programmes à compiler et les effets sémantiques de leur exécution, il ne tente pas de normaliser le modèle d'exécution binaire de C ++. La première fois que ce problème deviendra évident, c'est lorsqu'un client essaie de se lier à la bibliothèque d'importation de la DLL FastString à partir d'un environnement de développement C ++ autre que celui utilisé pour créer la DLL FastString.

Y a-t-il plus d'avantages ou la perte de ce manque de standardisation binaire?


Est-ce mieux posé sur programmers.stackexchange.com , vu que c'est plus une question subjective?
Stephen Furlani

1
Ma question connexe est en fait: stackoverflow.com/questions/2083060/…
AraK

4
Don Box est un fanatique. Ignore le.
John Dibling du

8
Eh bien, C n'est pas non plus standardisé par ANSI / ISO au niveau binaire; OTOH C a un ABI standard de facto plutôt que de jure . C ++ n'a pas une telle ABI standardisée car différents fabricants avaient des objectifs différents avec leurs implémentations. Par exemple, des exceptions dans VC ++ superposition sur Windows SEH. POSIX n'a ​​pas de SEH et donc prendre ce modèle n'aurait pas eu de sens (donc G ++ et MinGW n'utilisent pas ce modèle).
Billy ONeal

3
Je vois cela comme une caractéristique et non comme une faiblesse. Si vous liez une implémentation à un ABI spécifique, nous n'aurons jamais d'innovation et le nouveau matériel sera lié à la conception du langage (et comme il y a 15 ans entre chaque nouvelle version, cela fait longtemps dans l'industrie du matériel) et en étouffant innover de nouvelles idées pour rendre le code plus efficace ne sera pas fait. Le prix est que tout le code dans un exécutable doit être construit par le même compilateur / version (un problème mais pas majeur).

Réponses:


16

Les langages avec une forme compilée compatible binaire sont une phase relativement nouvelle [*], par exemple les runtimes JVM et .NET. Les compilateurs C et C ++ émettent généralement du code natif.

L'avantage est qu'il n'y a pas besoin d'un JIT, ou d'un interpréteur de bytecode, ou d'une VM, ou toute autre chose de ce genre. Par exemple, vous ne pouvez pas écrire le code d'amorçage qui s'exécute au démarrage de la machine comme un joli bytecode Java portable, à moins que peut-être la machine puisse exécuter nativement le bytecode Java, ou si vous avez une sorte de convertisseur de Java vers un natif non compatible binaire code exécutable (en théorie: pas sûr que cela puisse être recommandé dans la pratique pour le code bootstrap). Vous pouvez l'écrire en C ++, plus ou moins, bien qu'il ne soit pas portable C ++ même au niveau source, car il fera beaucoup de dégâts avec les adresses matérielles magiques.

L'inconvénient est que, bien sûr, le code natif ne s'exécute que sur l'architecture pour laquelle il a été compilé, et les exécutables ne peuvent être chargés que par un chargeur qui comprend leur format exécutable, et ne se lient et appellent à d'autres exécutables que pour la même architecture et ABI.

Même si vous arrivez à ce stade, la liaison de deux exécutables ne fonctionnera correctement que tant que: (a) vous ne violez pas la règle de définition unique, ce qui est facile à faire s'ils ont été compilés avec des compilateurs / options / autres, de telle sorte qu'ils utilisent différentes définitions de la même classe (soit dans un en-tête, soit parce qu'ils sont chacun liés statiquement à différentes implémentations); et (b) tous les détails d'implémentation pertinents tels que la disposition de la structure sont identiques selon les options du compilateur en vigueur lors de leur compilation.

Pour que la norme C ++ définisse tout cela, cela supprimerait une grande partie des libertés actuellement disponibles pour les implémenteurs. Les implémenteurs utilisent ces libertés, en particulier lors de l'écriture de code de très bas niveau en C ++ (et C, qui a le même problème).

Si vous voulez écrire quelque chose qui ressemble un peu à C ++, pour une cible portable binaire, il y a C ++ / CLI, qui cible .NET et Mono afin que vous puissiez (espérons-le) exécuter .NET ailleurs que Windows. Je pense qu'il est possible de persuader le compilateur de MS de produire des assemblages CIL purs qui fonctionneront en Mono.

Il y a aussi potentiellement des choses qui peuvent être faites avec par exemple LLVM pour créer un environnement C ou C ++ portable binaire. Je ne sais cependant pas qu'un exemple répandu ait émergé.

Mais tout cela repose sur la correction de beaucoup de choses que le C ++ rend dépendant de l'implémentation (comme la taille des types). L'environnement qui comprend les binaires portables doit alors être disponible sur le système sur lequel le code doit s'exécuter. En autorisant les binaires non portables, C et C ++ peuvent aller là où les binaires portables ne peuvent pas, et c'est pourquoi la norme ne dit rien du tout sur les binaires.

Ensuite, sur une plate-forme donnée, les implémentations ne fournissent généralement toujours pas de compatibilité binaire entre différents ensembles d'options, bien que la norme ne les arrête pas. Si Don Box n'aime pas que les compilateurs de Microsoft puissent produire des binaires incompatibles à partir de la même source, selon les options du compilateur, alors c'est l'équipe de compilateur dont il doit se plaindre. Le langage C ++ n'interdit un compilateur ou un OS de mettre le doigt sur tous les détails nécessaires, donc une fois que vous vous limitez à Windows , il est pas un problème fondamental avec C ++. Microsoft a choisi de ne pas le faire.

Les différences se manifestent souvent comme une autre chose que vous pouvez vous tromper et planter votre programme, mais il peut y avoir des gains considérables en efficacité entre, par exemple, des versions de débogage ou de sortie incompatibles d'une DLL.

[*] Je ne sais pas quand l'idée a été inventée pour la première fois, probablement 1642 ou quelque chose, mais leur popularité actuelle est relativement nouvelle, par rapport au moment où C ++ s'est engagé dans les décisions de conception qui l'empêchent de définir la portabilité binaire.


@Steve Mais C a un ABI bien défini sur i386 et AMD64, donc je peux passer un pointeur vers une fonction compilée par GCC version X vers une fonction compilée par MSVC version Y. Faire cela avec une fonction C ++ est impossible.
user877329

7

La compatibilité entre plateformes et entre compilateurs n'était pas le principal objectif de C et C ++. Ils sont nés à une époque, et destinés à des fins pour lesquelles les minimisations spécifiques à la plate-forme et au compilateur du temps et de l'espace étaient cruciales.

Extrait de "La conception et l'évolution de C ++" de Stroustrup:

"L'objectif explicite était de faire correspondre C en termes d'exécution, de compacité du code et de compacité des données. ... L'idéal - qui a été atteint - était que C avec des classes puisse être utilisé pour tout ce qui pourrait être utilisé."


1
+1 - exactement. Comment construire un ABI standard qui fonctionnerait à la fois sur des boîtiers ARM et Intel? Ça n'aurait pas de sens!
Billy ONeal

1
malheureusement, cela a échoué. Vous pouvez faire tout ce que fait C ... sauf charger dynamiquement un module C ++ au moment de l'exécution. vous devez «revenir» à l'utilisation des fonctions C dans l'interface exposée.
gbjbaanb

6

Ce n'est pas un bug, c'est une fonctionnalité! Cela donne aux implémenteurs la liberté d'optimiser leur implémentation au niveau binaire. Le petit endian i386 et sa progéniture ne sont pas les seuls processeurs qui existent ou existent.


6

Le problème décrit dans la citation est causé par le fait d'éviter délibérément la standardisation des schémas de manipulation des noms de symboles (je pense que la " standardisation au niveau binaire " est une expression trompeuse à cet égard bien que le problème soit lié à l' interface binaire d'application d' un compilateur ( ABI).

C ++ code les informations de signature et de type d'une fonction ou d'un objet de données, et son appartenance à la classe / l'espace de noms dans le nom de symbole, et différents compilateurs sont autorisés à utiliser des schémas différents. Par conséquent, un symbole dans une bibliothèque statique, une DLL ou un fichier objet ne sera pas lié au code compilé à l'aide d'un compilateur différent (ou peut-être même d'une version différente du même compilateur).

Le problème est décrit et expliqué probablement mieux que moi ici , avec des exemples de schémas utilisés par différents compilateurs.

Les raisons de l'absence délibérée de normalisation sont également expliquées ici .


3

Le but de l'ISO / ANSI était de normaliser le langage C ++, problème qui semble être suffisamment complexe pour nécessiter des années pour avoir une mise à jour des normes de langage et le support du compilateur.

La compatibilité binaire est beaucoup plus complexe, étant donné que les binaires doivent s'exécuter sur différentes architectures de CPU et différents environnements de système d'exploitation.


Certes, mais le problème décrit dans la citation n'a en fait rien à voir avec la "compatibilité au niveau binaire" (malgré l'utilisation du terme par l'auteur) dans un sens autre que de telles choses sont définies dans quelque chose appelé une "interface binaire d'application". Il décrit en fait la question des schémas de manipulation de noms incompatibles.

@Clifford: le schéma de changement de nom n'est qu'un sous-ensemble de la compatibilité au niveau binaire. ce dernier ressemble plus à un terme générique!
Nawaz

Je doute qu'il y ait un problème à essayer d'exécuter un binaire Linux sur une machine Windows. Les choses seraient bien meilleures s'il y avait un ABI par plate-forme, car au moins un langage de script pourrait charger et exécuter dynamiquement un binaire sur la même plate-forme, ou les applications pourraient utiliser des composants construits avec un compilateur différent. Vous ne pouvez pas utiliser une dll C sur linux aujourd'hui, et personne ne se plaint, mais cette dll C peut toujours être chargée par une application python, là où les avantages s'accumulent.
gbjbaanb

2

Comme Andy l'a dit, la compatibilité entre plates-formes n'était pas un grand objectif, tandis que la large mise en œuvre de la plate-forme et du matériel était un objectif, avec le résultat net que vous pouvez écrire des implémentations conformes pour une très large sélection de systèmes. La normalisation binaire aurait rendu cela pratiquement impossible.

La compatibilité C était également importante et aurait considérablement compliqué cela.

Il y a eu par la suite quelques efforts pour normaliser l'ABI pour un sous-ensemble d'implémentations.


Zut, j'ai oublié la compatibilité C. Bon point, +1!
Andy Thomas

1

Je pense que l'absence d'une norme pour C ++ est un problème dans le monde actuel de la programmation modulaire découplée. Cependant, nous devons définir ce que nous voulons d'une telle norme.

Personne sensé ne veut définir l'implémentation ou la plate-forme d'un binaire. Vous ne pouvez donc pas prendre une DLL Windows x86 et commencer à l'utiliser sur une plate-forme Linux x86_64. Ce serait un peu trop.

Cependant, ce que les gens veulent, c'est la même chose que nous avons avec les modules C - une interface standardisée au niveau binaire (c'est-à-dire une fois compilée). Actuellement, si vous souhaitez charger une DLL dans une application modulaire, vous exportez des fonctions C et vous les liez à l'exécution. Vous ne pouvez pas faire cela avec un module C ++. Ce serait formidable si vous le pouviez, ce qui signifierait également que les DLL écrites avec un compilateur pourraient être chargées par un autre. Bien sûr, vous ne pourrez toujours pas charger une DLL construite pour une plate-forme incompatible, mais ce n'est pas un problème à corriger.

Donc, si l'organisme de normalisation définissait l'interface d'un module, nous aurions beaucoup plus de flexibilité pour charger les modules C ++, nous n'aurions pas à exposer le code C ++ en tant que code C, et nous aurions probablement beaucoup plus d'utilisation de C ++ dans les langages de script.

Nous n'aurions pas non plus à subir des choses comme COM qui tentent de fournir une solution à ce problème.


1
+1. Oui je suis d'accord. Les autres réponses ici ont essentiellement éliminé le problème en disant que la normalisation binaire interdirait les optimisations spécifiques à l'architecture. Mais ce n'est pas le but. Personne ne plaide pour un format exécutable binaire multiplateforme. Le problème est qu'il n'y a pas d' interface standard pour charger dynamiquement les modules C ++.
Charles Salvia

1

Il y a beaucoup de problèmes de portabilité avec C ++, ce qui est uniquement dû au manque de standardisation au niveau binaire.

Je ne pense pas que ce soit aussi simple que cela. Les réponses fournies fournissent déjà une excellente justification du manque de concentration sur la normalisation, mais C ++ peut être trop riche en langage pour être bien adapté pour concurrencer véritablement C en tant que norme ABI.

Nous pouvons entrer dans le changement de nom résultant de la surcharge de fonctions, des incompatibilités de vtable, des incompatibilités avec des exceptions qui dépassent les limites des modules, etc.

Mais une norme ABI ne consiste pas seulement à rendre les dylibs C ++ produits dans un compilateur capables d'être utilisés par un autre binaire construit par un compilateur différent. ABI est utilisé dans plusieurs langues . Ce serait bien s'ils pouvaient au moins couvrir la première partie, mais je ne vois aucun moyen que C ++ soit vraiment en concurrence avec C au niveau ABI universel si crucial pour créer les dylibs les plus compatibles.

Imaginez une simple paire de fonctions exportées comme ceci:

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

... et imaginez Fooet Barétaient des classes avec des constructeurs paramétrés, des constructeurs de copie, des constructeurs de déplacement et des destructeurs non triviaux.

Ensuite, prenez le scénario d'un Python / Lua / C # / Java / Haskell / etc. développeur essayant d'importer ce module et de l'utiliser dans leur langue.

Tout d'abord, nous aurions besoin d'une norme de gestion de noms pour exporter des symboles en utilisant la surcharge de fonctions. C'est une partie plus facile. Pourtant, ce ne devrait pas vraiment être un nom de "mutilation". Étant donné que les utilisateurs de dylib doivent rechercher les symboles par nom, les surcharges ici devraient conduire à des noms qui ne ressemblent pas à un gâchis complet. Peut-être que les noms des symboles pourraient être similaires "f_Foo" "f_Bar_int"ou quelque chose de ce genre. Nous devons être sûrs qu'ils ne peuvent pas entrer en conflit avec un nom réellement défini par le développeur, réservant peut-être certains symboles / caractères / conventions à l'utilisation d'ABI.

Mais maintenant, un scénario plus difficile. Comment le développeur Python, par exemple, invoque-t-il des constructeurs de déplacement, des constructeurs de copie et des destructeurs? Nous pourrions peut-être les exporter dans le cadre du dylib. Mais que faire si Fooet Barsont exportés dans différents modules? Faut-il dupliquer ou non les symboles et implémentations associés dans ce dylib? Je suggérerais que nous le fassions, car cela pourrait devenir très ennuyeux très rapidement sinon de commencer à devoir être emmêlé dans plusieurs interfaces dylib juste pour créer un objet ici, le passer ici, le copier ici, le détruire ici. Alors que la même préoccupation de base pourrait quelque peu s'appliquer en C (juste plus manuellement / explicitement), C a tendance à éviter cela simplement par la manière dont les gens programment avec.

Ce n'est qu'un petit échantillon de la maladresse. Que se passe-t-il lorsque l'une des ffonctions ci-dessus lance une BazException(également une classe C ++ avec des constructeurs et des destructeurs et dérivant std :: exception) en JavaScript?

Au mieux, je pense que nous ne pouvons qu'espérer normaliser un ABI qui fonctionne d'un binaire produit par un compilateur C ++ à un autre binaire produit par un autre. Ce serait formidable, bien sûr, mais je voulais juste le souligner. Habituellement, le souci de distribuer une bibliothèque généralisée qui fonctionne avec des compilateurs croisés accompagne généralement le désir de la rendre vraiment généralisée et compatible.

Solution suggérée

Ma solution suggérée après avoir eu du mal à trouver des moyens d'utiliser des interfaces C ++ pour les API / ABI pendant des années avec des interfaces de style COM est de devenir un développeur "C / C ++" (jeu de mots).

Utilisez C pour créer ces ABI universels, avec C ++ pour l'implémentation. Nous pouvons toujours faire des choses comme des fonctions d'exportation qui renvoient des pointeurs vers des classes C ++ opaques avec des fonctions explicites pour créer et détruire de tels objets sur le tas. Essayez de tomber amoureux de cette esthétique C du point de vue ABI même si nous utilisons totalement C ++ pour la mise en œuvre. Les interfaces abstraites peuvent être modélisées à l'aide de tableaux de pointeurs de fonction. Il est fastidieux de regrouper ces éléments dans une API C, mais les avantages et la compatibilité de la distribution qui l'accompagne auront tendance à en faire très la peine.

Ensuite, si nous n'aimons pas autant utiliser cette interface directement (nous ne devrions probablement pas au moins pour des raisons RAII), nous pouvons envelopper tout ce que nous voulons dans une bibliothèque C ++ liée statiquement que nous livrons avec le SDK. Les clients C ++ peuvent l'utiliser.

Les clients Python ne voudront pas utiliser directement une interface C ou C ++ car il n'y a aucun moyen de les rendre pythoniques. Ils voudront l'intégrer dans leurs propres interfaces pythoniques, donc c'est en fait une bonne chose que nous exportions juste un strict minimum C API / ABI pour rendre cela aussi simple que possible.

Je pense que beaucoup de l'industrie C ++ gagnerait à le faire plutôt qu'à essayer de livrer obstinément des interfaces de style COM et ainsi de suite. Cela faciliterait également toute notre vie en tant qu'utilisateurs de ces dylibs pour ne pas avoir à se soucier des ABI maladroits. C le rend simple, et sa simplicité dans une perspective ABI nous permet de créer des API / ABI qui fonctionnent naturellement et avec minimalisme pour toutes sortes de FFI.


1
"Utilisez C pour créer ces ABI universels, avec C ++ pour l'implémentation." ... je fais de même, comme beaucoup d'autres!
Nawaz

-1

Je ne sais pas pourquoi il ne standardise pas au niveau binaire. Mais je sais ce que j'en fais. Sous Windows, je déclare la fonction extern "C" BOOL WINAPI. (Bien sûr, remplacez BOOL par le type de la fonction.) Et ils sont exportés proprement.


2
Mais si vous le déclarez extern "C", il utilisera le C ABI, qui est une norme de facto sur le matériel PC commun, même s'il n'est imposé par aucune sorte de comité.
Billy ONeal

-3

À utiliser unzip foo.zip && make foo.exe && foo.exesi vous souhaitez la portabilité de votre source.

En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.