Journal Les codes fantastiques (et où les trouver)

Posté par  (site web personnel) . Licence CC By‑SA.
Étiquettes :
42
1
juil.
2023

Demat' iNal,

Depuis fin 2022, j'anime une petite rubrique dans GNU/Linux Magazine, intitulée les codes fantastiques, où est décrit en une page une petite aventure informatique, un bout de code qui m'a amusé / plus / interpelé. [0]

Comme c'est les 25 ans de LinuxFR, et que j'en écris plus que ce qu'ils peuvent publier (trois d'avance pour le moment !), voici une sorte de ?hors série? que je ne publierai pas là bas du coup.

Les codes fantastiques : C curieux

Serge Guelton

Bricoleur interrogateur

Continuons cette série sur les codes fantastiques avec une illustration de l’outil clang-query.

Depuis C99, il est possible de déclarer une fonction C avec le mot-clef inline (mot-clef ironiquement hérité du C++). Ce mot-clef bien connu a deux effets : il suggère au compilateur de procéder à l’expansion du corps de la fonction sur les différents sites d’appels, et il donne à la fonction associée le linkage ODR, One Definition Rule, ce qui permet au symbole associé d’être présent dans plusieurs unité de compilation sans créer de conflit de nom lors de l’édition de lien.

Il arrive parfois que l’on déclare une fonction dans un fichier d’en-tête mais qu’on oublie de la marquer inline. Comment détecter cela lors des tests ? On va immédiatement passer aux oubliettes les solutions à base d’expressions régulières, puisqu’on peut utiliser un outil qui se base sur la libclang et qui se nomme clang-query.

>>> sudo dnf install /usr/bin/clang-query

Cet outil permet d’exprimer des requêtes sur un code, à la manière d’expressions régulières sur l’AST (Abstract Syntax Tree). Il fournit une REPL qui va nous permettre de jouer un peu, mais commençons par un exemple bateau pour naviguer en eaux calmes [1]:

// foo.h
#include <stdio.h> 
void foo() { puts("hello");}
inline void bar() { puts("demat"); }
extern void foobar();

Et regardons la session suivante :

$ clang-query foo.h --
clang-query> match functionDecl()
109 matches.
clang-query> match functionDecl(isExpansionInMainFile())
[...]
3 matches.
clang-query> match functionDecl(isExpansionInMainFile(), isDefinition())
[...]
2 matches.
Clang-query> match functionDecl(isExpansionInMainFile(), isDefinition(), unless(isInline()))

Match #1:

/home/ssp/articles/GLMF/00008-clang-tidy/foo.h:3:1: note: "root" binds here
void foo() { puts("hello");}
^~~~~~~~~~~~~~~~~~~~~~~~~~~~
1 match.

La première requête attrape toutes les fonctions, y compris celles venant de l’expansion de stdio.h. La deuxième applique un sélecteur supplémentaire sur l’origine de la fonction pour se concentrer sur les fonctions définie directement dans le fichier, mais elle attrape aussi foobar(). On limite donc la requête aux définitions pour le troisième essai, tandis que la quatrième exclut de la sélection les fonctions marquées inline. On peut sauvegarder ce script dans un fichier pour ensuite l’appeler de manière non-interactive :

$ clang-query -f detect-non-inline.query foo.h
[...]
$ cat detect-non-inline.query
match functionDecl(isExpansionInMainFile(), isDefinition(), unless(isInline()))

Ce petit exemple (légèrement amélioré) est utilisé dans l’intégration continue du projet xsimd. C’est donc un cas réel même s’il n’utilise qu’une toute partie des possibilités de clang-query que je vous invite à découvrir en lisant https://clang.llvm.org/docs/LibASTMatchersReference.html.

[0]: Liste des articles même si pour cette série là, ils ne sont pas encore passé sur une license CC-by-NC-ND

[1]: exemple bateau… eau trouble… qu'est ce qu'on rigole !

  • # Chouette journal

    Posté par  (site web personnel) . Évalué à 10. Dernière modification le 02 juillet 2023 à 16:49.

    Ah cool un journal de serge_sans_paille, je vais apprendre des trucs.

    Merci pour ce super journal qui me fait découvrir un nouvel outil :) Mais tu oublies de nous parler d'un autre moyen d'éviter le conflit de noms à l'édition des liens : utiliser le mot clé static. Et oui, ainsi la fonction aura la visibilité hidden dans chaque unité de compilation qui inclut l'entête et pour peu qu'il n'y ait pas de problème de gardes d'inclusions ça devrait passer.

    Bon évidemment il ne faut pas faire cela. Déjà parce que sémantiquement c'est tordu, et aussi parce qu'on passe pour un débutant qui débarque d'un de ces langages ésotériques où la déclaration et la définition sont dans le même fichier. Mais surtout, il faut éviter d'implémenter les fonctions dans les entêtes parce que ça crée du couplage entre l'implémentation de la fonction et le code qui inclut l'entête ; et y'en a ras la casquette de recompiler la terre entière dès qu'on touche à un entête, non mais, hein !

    À part ça je regardais le style-check.yml de xsimd et puisque j'en suis à donner mon avis alors que personne ne le demande, je vais vous dire deux choses les enfants : Premièrement, n'écoutez pas les inconnus qui affirment des trucs sur Internet. Deuxièmement : les étapes de votre CI doivent être lançables par les devs sur leurs machines. Ici on a un bon exemple puisque l'étape qui vérifie l'oubli d'inline dans les entêtes consiste à lancer un script bash dispo dans le dépôt. Je suis plus mitigé pour le clang-format qui lance une action GitHub tirée d'un autre dépôt ; impossible de lancer ça simplement en local. Quant à cross.yml et ses commandes shell qui s'étalent sur 40 lignes… bien malheureux celui qui aura à reproduire le build sur sa machine. À mon humble avis, ces 40 lignes devraient plutôt ressembler à sh ./test/cross-compile.sh --compiler ${matrix.sys.compiler} --compiler-version {matrix.sys.version} …

    Pourquoi faut-il sortir les scripts des fichiers de config de la CI vous demandez-vous?

    Déjà comme on vient de le voir, pour que les développeurs puissent lancer les tâches sur leur machines de développement. Pourquoi attendre d'avoir poussé sa branche, lancé la CI, construit 50 images Docker, exécuté 30 étapes de configs, pour au final apprendre que le fichier foo.cpp n'est pas bien formaté ; alors qu'on peut avoir l'info lors du commit en configurant une hook Git qui lance exactement le même script que la CI ? C'est une perte de temps pour les devs.

    D'autre part, vous souvenez-vous de Jenkins ? De Travis-CI ? Des autres outils de CI qui promettaient une config déclarative hyper simple ? Peu importe :) La CI ça va ça vient, et c'est bien dommage de lier ses tests avec une CI donnée. Que va-t-il se passer quand Elon Musk achètera GitHub et que tout le monde partira sur GitLab ? Il faudra encore réécrire tous ces fichiers de config. Adieu l'action clang-format, adieu cross.yml, il faut trouver autre chose. Mais on peut faire mieux ! Si tous ces tests étaient dans des scripts, ça marcherait partout ; et la config de la CI ne serait qu'une légère couche pour faire la glu entre les deux.

    Bon j'ai l'air d'un vieux râleur mais franchement merci pour ce journal, je suis impatient de lire les suivants :)

    • [^] # Re: Chouette journal

      Posté par  . Évalué à 5.

      Ton commentaire me fait penser que je cherche une fois l'an un remplaçant à make, que jamais je ne trouve :)

      Discussions en français sur la création de jeux videos : IRC freenode / #gamedev-fr

      • [^] # Re: Chouette journal

        Posté par  (Mastodon) . Évalué à 3.

        Parce que make est un moyen et non une fin.
        Tu utilises make comme tu veux, mais d'autres outils se plaçant sur le même créneau se veulent « la solution pour les remplacer toutes, et selon leur méthode nous faire travailler. »

        Je n'ai jamais été très réceptif à « voilà comment il faut faire ça », alors que toujours beaucoup à « tiens, avec ce truc tu peux faire ça ! ».

        Mais peut-être mk, le make de plan9 ? :)

        • Yth.
        • [^] # Re: Chouette journal

          Posté par  (site web personnel) . Évalué à 5.

          Parce que make est un moyen et non une fin.

          Applicable au début de la discussion sur travis / jenkins / github / gitlab / … Le but est d'avoir de l'intégration continue. Le moyen est un logiciel ou un autre, et sa bonne utilisation (déclencher uniquement ce qui est nécessaire, obtenir des résultats rapidement, etc.)

      • [^] # Re: Chouette journal

        Posté par  (site web personnel) . Évalué à 3.

        Perso j'ai convergé vers CMake + Ninja + Ccache + Bash. Le premier s'améliore sans cesse et répond à quasiment tous les cas. Pour le second j'ai une légère préférence par rapport à make car il ne flood pas mon terminal, et je peux utiliser ninjatracing avec. Le troisième me rend la vie plus agréable quand je passe d'une branche à l'autre.

        Et puis il y a Bash pour tout le reste. Par exemple, Ccache a beau être très efficace il ne gère pas le link, donc j'en viens souvent à mettre en place un petit cache de binaires pour les dépendances ou pour des builds complets. Par exemple au boulot sur la CI on se compare à une branche de référence. Le fait de conserver les artefacts de build et de les restaurer plutôt que de recompiler ladite branche fait gagner environ 15 minutes (sur 15) pour les builds suivants. Sur un projet perso j'ai le même genre de choses pour les dépendances.

        De même pour les 50 options CMake des multiples variantes de build. Plutôt que de configurer ça à la main je fais un petit script que je lance avec l'option qui va bien pour lancer la config qui correspond ; par exemple ./setup.sh --build-type tsan configure le projet en debug, active ThreadSanitizer, et lance le build. Ça n'empêche pas d'utiliser CMake pour les intégrateur mais pour mon quotidien ça simplifie bien la vie.

      • [^] # Re: Chouette journal

        Posté par  (site web personnel) . Évalué à 1.

        Je me suis noté que le langage Nim propose un interpréteur et une bibliothèque de fonction assez proche du shell. Je garde donc dans un coin de ma tête l'envie d'explorer ça comme remplacement à Make… Nim c'est du "Python" typé et compilable, j'ai plus le truc en tête mais y'a ce qu'il faut pour paralléliser facilement.

        Bon je suis resté avec mes Makefile jusque là, malgré le côté pénible du "un shell par ligne", parce que c'est suffisant.

        Enfin, mon usage du Makefile c'est plus un fourre-tout des commandes utiles du projet (je ne compile pas grande chose au quotidien).

        La liberté ne s'use, que si on ne s'en sert pas.

      • [^] # Re: Chouette journal

        Posté par  . Évalué à 1.

        Moi je suis passé à just qui fait bien mieux une grande partie du boulot de make (définir des commande accessible pour un projet donné) tout en laissant de côté les truc qui sont clairement mieux fait ailleurs et complique tout (laissons ccache faire son boulot il le fait bien).

        Je ne suis jamais revenu en arrière depuis : la doc est bien faite, le projet est actif, le dev réactif et c'est vraiment simple à utiliser.

        • [^] # Re: Chouette journal

          Posté par  . Évalué à 2. Dernière modification le 06 juillet 2023 à 09:33.

          J'ai essayé just , mais il ne fait pas le suivi des dépendances.

          C'est à dire qu'un régle telle que:
          %.o: %.c

          ne fonctionnera pas.

          J'ai vérifié auprès de l'auteur qui m'a confirmé que ce n'était pas possible.

          L'idée d'utiliser les possibilités de Nim , évoquée ci-dessus, me parait un bien meilleur plan. D'autant que même sans utiliser le système de macros de Nim, on peut l'utiliser comme un langage de script.

          Discussions en français sur la création de jeux videos : IRC freenode / #gamedev-fr

        • [^] # Re: Chouette journal

          Posté par  . Évalué à 3.

          Moi je suis passé à just qui fait bien mieux une grande partie du boulot de make

          Je l'ai vu passer plusieurs fois récemment et j'ai pas compris ce qu'il apporte par rapport à make

          https://linuxfr.org/users/barmic/journaux/y-en-a-marre-de-ce-gros-troll

  • # inline ou pas

    Posté par  . Évalué à 2. Dernière modification le 07 juillet 2023 à 16:46.

    Ce mot-clef bien connu a deux effets : il suggère au compilateur de procéder à l’expansion du corps de la fonction

    Il me semble avoir lu à plusieurs reprise qu'il y a bien longtemps que le mot clé inline n'était plus utilisé par le compilateur pour décider quelle fonction inliner.

    alors je suis allé voir la doc pour sourcer la bêtise que je m'apprétais à écrire dans ce message et….. c'est pas clair.

    si je m'en tiens à la doc de gcc sur le mot clé inline il semble qu'il est bien utilisé pour définir quelle fonction inliner.

    Par contre, quand je vais voir les options de la ligne de commande de gcc il explicite bien à chaque fois que c'est sur la base d'heuristiques du compilateur que sont choisies quelles fonctions inliner.

    On peut arguer qu'un mot clé dans le code est une heuristique, mais si ça n'était que cela, je pense qu'il aurait suffit d'écrire ça dans la doc des options.

    quelqu'un aurait une référence sur ce que fait exactement gcc sans aller lire le code ?

Suivre le flux des commentaires

Note : les commentaires appartiennent à celles et ceux qui les ont postés. Nous n’en sommes pas responsables.