- Le code qui suit uniquement la norme ISO C est rare, et les bases de code C réelles dépendent d’extensions non standard pour ajouter des fonctionnalités et contourner des lacunes propres aux compilateurs et bibliothèques
- Un compilateur C utile doit au minimum pouvoir traiter des en-têtes système comme
<stdio.h>, mais glibc crée une barrière avec des extensions GNU et des hypothèses comme__attribute__((packed))et#include_next - La logique de permutation d’octets de SDL peut choisir de l’assembleur inline si des macros d’ISA sont présentes, ce qui peut imposer des extensions de style GCC même à des compilateurs autres que GCC ou clang
- La gestion de
extern inlinedans OpenBSD et Gnulib complique la compatibilité de la sémantique de inline à cause des différences entre C99 et GCC, des branches par plateforme et des conditions liées à_FORTIFY_SOURCE - Un petit compilateur C doit choisir entre des correctifs upstream, des correctifs downstream, l’obtention de gardes dédiées, ou l’imitation de la compatibilité GCC, tandis qu’un usage plus large des macros de test de fonctionnalités semble être une meilleure direction
La première barrière créée par les en-têtes glibc
- Pour être un compilateur C utile, il faut pouvoir prétraiter et analyser les en-têtes de la bibliothèque C système, et si l’on ne peut pas traiter
<stdio.h>, il est difficile de faire passer ne serait-ce qu’un hello world - Dans un environnement GNU/Linux, cette barrière mène à glibc
- glibc détermine les extensions prises en charge en examinant les macros prédéfinies par le compilateur dans sys/cdefs.h, un fichier indirectement inclus par presque tous les en-têtes libc
- Les extensions non prises en charge sont traitées en supprimant les définitions correspondantes, mais cette logique de compatibilité peut elle-même se casser dans la pratique
-
struct epoll_eventet__attribute__((packed))- Le
struct epoll_eventdesys/epoll.hsur Linux est une packed struct qui utilise le GNU__attribute__((packed)) - Cet attribut modifie l’agencement de la structure en 64 bits, donc l’ignorer casse l’ABI
- Il ne suffit pas que le compilateur implémente
__attribute__((packed)) sys/cdefs.hcontient du code qui définit__attribute__(xyz)comme une macro vide si le compilateur n’est ni GCC, ni clang, ni tcc- Résultat, un autre compilateur peut voir cet attribut supprimé dans les en-têtes glibc même s’il le prend en charge
- On peut objecter que l’en-tête
epollest spécifique à Linux, donc qu’il est difficile d’y appliquer tel quel les critères de portabilité de la norme C
- Le
-
limits.het#include_next- Certains en-têtes C comme
stddef.h,stdint.h,limits.hetfloat.hsont requis même dans une implémentation freestanding, donc le compilateur doit les fournir - POSIX exige que
limits.hdéfinisse aussi des constantes propres à POSIX en plus des constantes standard du C, ce qui nécessite unlimits.hspécifique à la plateforme au-dessus de celui du compilateur - Le
<limits.h>de glibc définit directement les valeurs ANSIlimits.hen dehors de GNU C, et dans un environnement GCC il récupère l’en-tête du compilateur via#include_next <limits.h> - Cette structure suppose que le
limits.hbuiltin spécifique à GCC définit certaines macros, et dépend aussi de l’extension#include_next - clang doit lui aussi contourner cette structure
- Certains en-têtes C comme
Détection de fonctionnalités dans SDL et problème d’assembleur inline
- Les fonctions de permutation d’octets de
SDL_endian.hutilisent si possible des builtins du compilateur ou de l’assembleur inline, puis se rabattent en dernier recours sur une implémentation générale à base d’opérations sur les bits - La logique de détection fonctionne en gros dans cet ordre
- si c’est GCC ou clang et que
__has_builtin(__builtin_bswapX)existe, utiliser le builtin - si c’est MSVC 8.0 ou plus, utiliser l’intrinsic MSVC via
#pragma - si des macros propres à une ISA comme
__x86_64__sont définies, utiliser de l’assembleur inline - sinon, utiliser l’implémentation générale à base d’opérations sur les bits
- si c’est GCC ou clang et que
- Cet ordre devient problématique si un compilateur qui n’est ni GCC ni clang définit pour de bonnes raisons des macros prédéfinies propres à une ISA
- Même si ce compilateur fournit un builtin
bswapet l’opérateur spécial__has_builtin, la logique peut malgré tout tenter d’utiliser de l’assembleur inline de style GCC - En conséquence, la structure suppose qu’un compilateur inconnu prend aussi en charge l’assembleur inline au format GCC
La confusion autour de extern inline dans libc OpenBSD
- Certains en-têtes d’OpenBSD contiennent des définitions de fonctions inline que le compilateur peut utiliser de manière optionnelle quand l’optimisation est activée
- Ces fonctions sont définies via la macro
__only_inline, et si le compilateur ne les inline pas réellement, il faut retomber sur un symbole externe - Autrement dit, il faut des fonctions inline avec liaison externe
-
Différences entre inline C99 et inline GCC
inlineest spécifié en C99, mais son comportement standard entre en conflit avec le comportement non standard de GCC avant C99- Dans un en-tête, une définition inline doit utiliser
extern inlineavec le corps de fonction, et dans ce cas elle n’émet pas la véritable fonction exportée - Dans une unité de traduction, il faut déclarer la fonction avec seulement
inlinepour exporter la définition - Le sens de
inlinediffère aussi entre C++ et C - Ces différences sont détaillées dans l’article de Youtao Guo
-
__only_inlinedans OpenBSD- OpenBSD dépend de la sémantique inline de GCC
- Pour masquer les différences entre versions de GCC, la macro
__only_inlinede sys/cdefs.h spécifie explicitement l’ancienne sémantiquegnu89inline sur les GCC récents via__attribute__ - Sur les compilateurs non GNU,
__only_inlineest défini avec une liaisonstatic - Le résultat est que les fonctions peuvent être déclarées et définies avec des liaisons contradictoires, ce qui casse la compilation
-
Le contournement
_ANSI_LIBRARY- OpenBSD respecte la macro
_ANSI_LIBRARY - Si cette macro est définie, il omet complètement les définitions
__only_inlineproblématiques dans les en-têtes standard commesignal.h - On perd la version optimisée, mais au moins la compilation fonctionne
- OpenBSD respecte la macro
-
Le code de compatibilité
extern inlinede Gnulib- Le code de compatibilité
extern inlinede Gnulib apparaît aussi lors de la compilation de Guile et nano - extern-inline.m4 contient des branches conditionnelles complexes pour gérer des implémentations cassées et étranges de ce corner case du C
- Ces conditions reflètent des différences d’environnement comme Apple, DragonFly, FreeBSD, GCC, clang, PCC, HP cc, PGI, SunPro C,
_FORTIFY_SOURCE,__GNUC_STDC_INLINE__et__GNUC_GNU_INLINE__
- Le code de compatibilité
L’hypothèse clang dans bionic d’Android
- bionic est la libc d’Android, et ses en-têtes supposent clang encore plus fortement que GCC
- Les en-têtes bionic utilisent largement des extensions spécifiques à clang comme
_Nonnullet_Null_unspecifiedpour les nullability checks - Il n’est pas très difficile de neutraliser ces macros par un
#definepassé en ligne de commande - Ce problème apparaît dans les en-têtes bionic quand on utilise un téléphone Android comme environnement de développement natif aarch64 via Termux
_Null_unspecifiedest aussi appelé__BIONIC_COMPLICATED_NULLNESS, et la définition correspondante se trouve dans le sys/cdefs.h de bionic
Les choix auxquels fait face un petit compilateur C
- Le code qui suit uniquement la norme ISO C est rare dans la pratique, et beaucoup de bases de code C dépendent de comportements non standard et d’extensions du langage
- Cette dépendance ne vient pas seulement de fonctionnalités supplémentaires, mais aussi de la nécessité de contourner des bugs et lacunes différents selon les compilateurs et bibliothèques
- Les bases de code qui veulent prendre en charge plusieurs environnements dépendent de tests du préprocesseur et de gardes, mais cette méthode se casse facilement et reste difficile à gérer
- Lorsqu’on développe un compilateur C comme antcc, ces problèmes de compatibilité réapparaissent sans cesse
- Quand de nombreux projets open source dépendent, même pour des besoins non essentiels, d’extensions et de comportements non standard propres à un compilateur, la charge de prise en charge augmente pour les compilateurs alternatifs
- En même temps, il est difficile d’exiger que tous les développeurs testent leur code C sur plusieurs compilateurs, y compris des compilateurs petits et peu connus
- La portabilité du C est déjà suffisamment difficile en soi
- Du point de vue de l’auteur d’un compilateur, il existe quatre options possibles
- essayer de faire accepter des correctifs upstream pour les incompatibilités
- devenir suffisamment connu pour que les développeurs ajoutent des tests de base et des vérifications
#ifdefdédiées - gérer cela downstream et distribuer des patches ou des correctifs séparés
- prétendre être une certaine version de GCC et implémenter les extensions correspondantes
- Les correctifs upstream semblent être un combat difficile à gagner, et les correctifs downstream sont la solution la plus facile
- Pour prendre en charge beaucoup de bases de code avec un minimum de confusion pour les utilisateurs et les développeurs, l’imitation de la compatibilité GCC est réaliste, mais coûteuse à implémenter
- clang définit
__GNUC__=4,__GNUC_MINOR__=2et__GNUC_PATCHLEVEL__=1pour revendiquer une compatibilité GCC 4.2.1 - Aujourd’hui clang est presque un cas de prise en charge séparé, mais il a fallu un gros effort, avec des correctifs dans les deux projets, rien que pour permettre la compilation du noyau Linux avec clang
Les macros GCC et le problème du rattrapage
- La stratégie qui consiste à se faire passer pour GCC a elle aussi ses problèmes
- Beaucoup de bases de code se contentent de tester
#ifdef __GNUC__et peuvent utiliser des extensions GCC récentes sans vérifier la version - Dans ce cas, le compilateur alternatif doit continuer à rattraper l’écart
- C’est l’une des raisons pour lesquelles clang, bien qu’il prenne en charge des extensions GNU plus récentes que 4.2.1, n’augmente pas la valeur de ses macros
__GNUC__ - Le contexte correspondant est abordé dans la discussion LLVM sur l’augmentation de la version mineure de
__GNUC__
Une meilleure direction et l’état actuel
- Idéalement, à la place des gardes spécifiques aux compilateurs et des vérifications de version, il faudrait utiliser plus largement des macros de test de fonctionnalités
- Parmi les macros de test utiles, on trouve
__has_builtin,__has_featureet__has_attribute - On pourrait aussi davantage utiliser des macros standard comme
__STDC_NO_VLA__ - Dans le monde *NIX actuel, pour le meilleur ou pour le pire, le quasi-duopole GCC/clang est l’état de fait
- Le développement de petits compilateurs C indépendants continue aussi
1 commentaires
Avis sur Lobste.rs
(auteur du compilateur kefir) D’après mon expérience, le problème de
__attribute__dans<sys/cdefs.h>fait partie des cas les plus pénibles. Cela casseepoll, les structurespackedcourantes, les constructeurs et la visibilité des symboles, si bien que j’ai dû embarquer cet en-tête monkey patch avec kefirCe n’est pas idéal, mais c’est probablement l’approche la plus réaliste, et en pratique cela nous a permis d’éliminer la plupart des patches personnalisés dans les suites de test externes
Un autre type d’échec, ce sont les codes alternatifs bogués. Certains projets essaient de détecter le compilateur pour s’adapter, mais comme les compilateurs alternatifs sont peu testés, le code de fallback est parfois plein de bugs ou mal maintenu. Du point de vue d’un auteur de compilateur, c’est bien plus agaçant qu’un échec immédiat avec « compilateur non pris en charge ». Par exemple, il faut alors déboguer soi-même d’étranges mauvaises compilations, comme des incohérences de largeur de
typedefd’entiers entre le programme et une bibliothèque précompilée$TERMsurxterm-256colorpour faire semblant d’être xterm, tout part en vrilleJe ne vois vraiment pas comment résoudre ça. Au final, faut-il simplement que notre projet devienne assez répandu et connu ? Facile !
J’ai aussi l’impression d’avoir déjà rencontré plusieurs fois ces mauvaises compilations bizarres causées par des fallbacks de détection de compilateur mal maintenus, et c’est vraiment pénible
Je développe surtout cproc sur linux-musl, donc je ne savais pas que glibc désactivait
__attribute__avec d’autres compilateurs, mais en pratique c’est une situation assez mauvaise. Les commentaires disent qu’on peut ignorer l’usage des attributes sans problème, mais ils ne tiennent pas compte du fait que la plupart des applications incluentsys/cdefs.hindirectement et peuvent utiliser des attributes qu’on ne peut justement pas ignorerEn plus de
packed,alignedetconstructorsont aussi couramment utilisésJe me demande si cela a été signalé quelque part dans un gestionnaire de tickets. La plupart des usages d’attributes dans
cdefs.hsemblent déjà protégés par__glibc_has_attribute, donc je me demande ce que l’on gagne réellement à désactiver globalement__attribute__, et si on pourrait supprimer çaLes fonctionnalités utilisées par les en-têtes de la libc que le compilateur n’a pas de bon moyen d’indiquer comme prises en charge posent aussi problème. Ce sont des fonctionnalités qui ne se révèlent pas via
__has_attributeou__has_builtin, par exemple les labels__asm__. NetBSD les utilise pour renommer les symboles, et déclenche#errorsi__GNUC__ou__PCC__n’est pas défini. Cela dit, je ne sais pas vraiment quoi proposer d’autre que de simplement essayer et laisser échouer si ce n’est pas pris en chargeJ’ai aussi eu des problèmes liés à
__builtin_va_list. Certaines libc, sans__GNUC__, définissentva_listcommevoid *, ou vont même jusqu’à fournir des définitions incompatibles. Là encore, on ne peut pas tester cela avec__has_builtin.__has_builtin(__builtin_va_arg)pourrait peut-être être un test suffisamment bon, mais je ne vois pas bien comment faire corriger cela sur macOS__attribute__dans/usr/include/syset/usr/include/bits, j’en ai trouvé beaucoup qui n’étaient pas protégés. C’était surtout__format__,__aligned__et__noreturn__, donc il faudrait aussi corriger ceux-làglibc ne semble globalement pas faire de la compatibilité avec les compilateurs non GCC une priorité, donc je ne sais pas si ce genre de patch serait accepté. Après une mise à niveau du système au début de l’année, glibc a ajouté dans les en-têtes Linux un usage non protégé de
__SIZE_TYPE__, ce qui a empêché mon compilateur de compiler certains projets. Je l’ai signalé, mais ce n’est toujours pas corrigé, et j’ai fini par ajouter des macros prédéfinies de style__X_TYPE__pour m’aligner sur GCCPour le problème des labels
__asm__, je ne vois pas de bonne solution. Cela dit, si le renommage de nom asm est vraiment indispensable au fonctionnement, il vaut peut-être mieux simplement essayer et laisser échouer plutôt que de faire une vérification du compilateur__builtin_va_listest assez grave. Je pensais que__has_builtin(__builtin_va_list)fonctionnerait, mais apparemment ce n’est pas le cas