1 points par GN⁺ 17 시간 전 | 1 commentaires | Partager sur WhatsApp
  • 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 inline dans 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_event et __attribute__((packed))

    • Le struct epoll_event de sys/epoll.h sur 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.h contient 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 epoll est spécifique à Linux, donc qu’il est difficile d’y appliquer tel quel les critères de portabilité de la norme C
  • limits.h et #include_next

    • Certains en-têtes C comme stddef.h, stdint.h, limits.h et float.h sont requis même dans une implémentation freestanding, donc le compilateur doit les fournir
    • POSIX exige que limits.h définisse aussi des constantes propres à POSIX en plus des constantes standard du C, ce qui nécessite un limits.h spécifique à la plateforme au-dessus de celui du compilateur
    • Le <limits.h> de glibc définit directement les valeurs ANSI limits.h en 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.h builtin spécifique à GCC définit certaines macros, et dépend aussi de l’extension #include_next
    • clang doit lui aussi contourner cette structure

Détection de fonctionnalités dans SDL et problème d’assembleur inline

  • Les fonctions de permutation d’octets de SDL_endian.h utilisent 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
  • 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 bswap et 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

    • inline est 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 inline avec 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 inline pour exporter la définition
    • Le sens de inline diffère aussi entre C++ et C
    • Ces différences sont détaillées dans l’article de Youtao Guo
  • __only_inline dans OpenBSD

    • OpenBSD dépend de la sémantique inline de GCC
    • Pour masquer les différences entre versions de GCC, la macro __only_inline de sys/cdefs.h spécifie explicitement l’ancienne sémantique gnu89 inline sur les GCC récents via __attribute__
    • Sur les compilateurs non GNU, __only_inline est défini avec une liaison static
    • 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_inline problématiques dans les en-têtes standard comme signal.h
    • On perd la version optimisée, mais au moins la compilation fonctionne
  • Le code de compatibilité extern inline de Gnulib

    • Le code de compatibilité extern inline de 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__

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 _Nonnull et _Null_unspecified pour les nullability checks
  • Il n’est pas très difficile de neutraliser ces macros par un #define passé 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_unspecified est 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 #ifdef dé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__=2 et __GNUC_PATCHLEVEL__=1 pour 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_feature et __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 casse epoll, les structures packed courantes, les constructeurs et la visibilité des symboles, si bien que j’ai dû embarquer cet en-tête monkey patch avec kefir
    Ce 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 typedef d’entiers entre le programme et une bibliothèque précompilée

    • Il se passe quelque chose de similaire dans les terminaux. Si on ne définit pas $TERM sur xterm-256color pour faire semblant d’être xterm, tout part en vrille
      Je ne vois vraiment pas comment résoudre ça. Au final, faut-il simplement que notre projet devienne assez répandu et connu ? Facile !
    • L’approche par en-tête monkey patch semble aussi être celle utilisée par slimcc, et ça paraît être un compromis plutôt correct
      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 incluent sys/cdefs.h indirectement et peuvent utiliser des attributes qu’on ne peut justement pas ignorer
    En plus de packed, aligned et constructor sont aussi couramment utilisés
    Je me demande si cela a été signalé quelque part dans un gestionnaire de tickets. La plupart des usages d’attributes dans cdefs.h semblent 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 ça
    Les 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_attribute ou __has_builtin, par exemple les labels __asm__. NetBSD les utilise pour renommer les symboles, et déclenche #error si __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 charge
    J’ai aussi eu des problèmes liés à __builtin_va_list. Certaines libc, sans __GNUC__, définissent va_list comme void *, 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

    • Après une recherche rapide des usages de __attribute__ dans /usr/include/sys et /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 GCC
      Pour 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_list est assez grave. Je pensais que __has_builtin(__builtin_va_list) fonctionnerait, mais apparemment ce n’est pas le cas