1 points par GN⁺ 2024-03-01 | 1 commentaires | Partager sur WhatsApp
  • Si vous reprenez une base de code C++ legacy vaste et complexe, l’objectif n’est pas d’obtenir un « code propre », mais d’amener la sécurité, l’expérience développeur, la justesse et les performances à un niveau acceptable
  • Le point de départ consiste à obtenir l’adhésion de l’organisation et une limite de temps, à consigner les plateformes prises en charge dans le README, puis à faire en sorte que la compilation locale et les tests passent de manière fiable
  • L’amélioration de la vitesse de build et de test doit commencer par des améliorations faciles mesurées, comme supprimer les tests des dépendances, nettoyer les cibles inutiles ou essayer un linker rapide comme mold
  • Après avoir réduit le code inutilisé et le code destiné à des plateformes non prises en charge, il faut mettre en place une boucle de validation automatique avec clang-tidy, cppcheck, clang-format, -fsanitize=address,undefined et la CI
  • La réécriture ou l’adoption d’un standard C++ plus récent ne sont pas des objectifs mais des moyens, et une réécriture dans un langage à mémoire sûre ne mérite d’être envisagée qu’en présence de raisons solides

Au début, on met de l’ordre dans les personnes et les प्रक्रdures avant le code

  • Améliorer une base de code C++ legacy ne doit pas être un effort solitaire mené le soir après le travail ni une « marche de la mort » de longue durée, mais un travail d’ingénierie logicielle soutenable
  • Il faut expliquer les problèmes, les solutions et les contraintes de temps à son manager, à ses collègues et aux personnes non techniques en termes simples
    • Si une nouvelle recrue a mis 3 semaines à réussir un build local et à faire sa première contribution, on peut proposer de ramener cela à quelques minutes
    • Si une configuration simple de fuzzing fait planter l’application 253 fois en quelques secondes, cela fournit un argument concret pour expliquer le risque en production
    • Si le déploiement dépend d’un unique serveur de build FreeBSD 9 non pris en charge depuis 8 ans, une panne de ce serveur bloque les livraisons
    • Si un linter standard du secteur détecte immédiatement un comportement indéfini à l’origine de bugs en production, il y a de bonnes raisons de l’exécuter à chaque changement
    • Si une bibliothèque de chiffrement copiée et modifiée à la main empêche d’évaluer l’impact des vulnérabilités, il faut rationaliser les dépendances et mettre en place des alertes automatiques
  • Les approches à éviter sont également claires
    • Faire une mise à niveau complète vers le dernier standard C++ pendant 2 semaines alors qu’il n’y a pas de tests
    • Produire pendant des mois de gros changements dans une branche séparée en espérant qu’ils seront fusionnés un jour
    • Démarrer une réécriture complète dès le départ en supposant que tout sera terminé en quelques semaines
    • Lancer une « amélioration de la base de code » sans savoir quoi faire ni pour quand

Une manière sûre d’améliorer

  • Tous les changements doivent être petits et incrémentaux, et l’application doit fonctionner avant comme après, avec des tests et des linters qui passent
  • Les correctifs urgents doivent rester possibles comme avant, sans être bloqués par le travail d’amélioration
  • Chaque changement doit apporter une amélioration mesurable et pouvoir être expliqué ou démontré même à des non-spécialistes
  • Même si l’ensemble du travail est interrompu à cause des priorités ou du budget, il doit rester un bénéfice net mesurable par rapport à l’état initial

Indiquer les plateformes prises en charge dans le README

  • Le README doit lister les couples <architecture>-<operating-system> officiellement pris en charge
    • Par exemple : x86_64-linux, aarch64-darwin
  • Cette liste sert de référence pour vérifier que le projet compile sur toutes les plateformes prises en charge, et de justification plus tard pour supprimer le code destiné à des plateformes non prises en charge
  • Si nécessaire, on peut aussi préciser des versions d’architecture comme ARMv6 ou ARMv7
  • La liste des plateformes prises en charge permet de répondre à des questions comme :
    • Peut-on dépendre du support matériel pour les nombres en virgule flottante, SIMD ou SHA256 ?
    • Faut-il prendre en charge le 32 bits ?
    • Le logiciel tourne-t-il sur des plateformes big-endian ?
    • Faut-il envisager que char puisse être sur 7 bits ?
  • Les stations de travail des développeurs doivent absolument figurer dans cette liste

Stabiliser d’abord la compilation locale et les tests

  • Même des bases de code C++ au cœur de produits à succès ne compilent souvent pas de façon fiable, et l’objectif n’est pas un « build qui marche parfois », mais un build cohérent sur toutes les plateformes prises en charge
  • La meilleure situation est de pouvoir compiler et exécuter directement sur la machine du développeur
    • Si le projet est trop gros et manque de RAM, on peut louer un gros serveur pour les builds
    • Si une API spécifique à la plateforme comme io_uring est nécessaire, on peut implémenter un shim ou builder dans une machine virtuelle sur le poste de travail
    • Malgré tout, le build local direct reste la meilleure option
  • S’il n’y a pas de tests, il faut d’abord en écrire avant de modifier le code
    • Le point de départ le plus simple consiste souvent à capturer les entrées et sorties du programme réel pour créer des tests end-to-end
    • Ces tests ne garantissent pas que le comportement existant soit correct, mais ils limitent les régressions lors des changements
  • S’il existe une suite de tests mais que certains échouent, il faut d’abord les désactiver et obtenir un état où l’ensemble passe, même si cela prend des heures

Documenter la manière de builder et de tester

  • Le README doit expliquer comment builder et tester l’application
  • La forme idéale est une seule commande de build et une seule commande de test
  • Si la procédure est complexe au départ, on peut masquer cette complexité derrière build.sh et test.sh
  • L’objectif est qu’une personne qui n’est pas experte C++ puisse lancer le build et les tests sans poser de questions
  • Il vaut mieux documenter la structure du projet ou l’architecture plus tard, une fois le code inutile supprimé

Réduire le temps de build et de test avec des améliorations faciles

  • Avant de refondre le système de build ou de tenter des optimisations héroïques, il faut mesurer des améliorations faciles
  • Premiers points à vérifier
    • Vérifier si les tests des dépendances sont rebuildés et relancés à chaque fois
      • Avec unittest++ utilisé comme sous-projet CMake, on a vu des cas où même les tests du framework de test étaient rebuildés et exécutés à chaque fois
    • Vérifier si les programmes d’exemple des dépendances sont buildés et exécutés
      • mbedtls permettait aussi de désactiver le build des exemples via une variable CMake
    • Vérifier si votre projet, lorsqu’il est inclus comme sous-projet d’un autre, build ses tests et exemples par défaut
      • Il est recommandé de désactiver par défaut des variables de build comme MYPROJECT_TEST, et de ne les activer que pendant le développement direct
    • Vérifier si toutes les dépendances tierces sont buildées alors qu’en pratique seule une petite partie est utilisée
      • mbedtls fournit beaucoup de flags de compilation pour désactiver ce qui n’est pas nécessaire
    • Vérifier si les dépendances entre cibles sont incorrectes et forcent un rebuild complet à cause d’un petit changement
      • De nombreux systèmes de build peuvent afficher le graphe de dépendances
    • Essayer un linker rapide comme mold
    • Comparer aussi les compilateurs quand c’est possible
      • Sur certains projets, clang a été 2 fois plus rapide que gcc ; sur d’autres, il n’y avait pas de différence
  • Autres pistes à tester, mais dont le gain peut être faible voire négatif
    • Activer/désactiver/thin sur LTO
    • Séparer les informations de debug
    • Comparer Make et Ninja
    • Tester le type de système de fichiers et ses réglages
  • Si les builds prennent trop de temps, modifier le code devient en soi irréaliste

Supprimer le code inutile

  • Dans certains cas, plus de 30 % de la base de code était totalement morte, et ce code continue d’augmenter les coûts de compilation et de refactorisation
  • Méthodes de suppression
    • Utiliser les warnings -Wunused-xxx du compilateur
      • Par exemple : -Wunused-function
      • Le plus souvent, il suffit de supprimer puis de recompiler et retester, mais cela peut plus rarement signaler un bug où la mauvaise fonction est appelée ; il faut donc se méfier d’une automatisation totale
    • Utiliser un linter comme cppcheck pour trouver des fonctions inutilisées ou des champs de classe non utilisés
      • Il peut y avoir beaucoup de faux positifs avec l’héritage et les fonctions virtuelles, mais l’outil peut repérer des éléments inutilisés que le compilateur ne voit pas
    • On peut aussi demander au linker de placer chaque fonction dans une section séparée et de signaler les sections inutilisées, mais le bruit venant des fonctions de la bibliothèque standard peut rendre cela peu pratique
    • Comparer l’assembleur généré avec le code source fonctionne mal avec les fonctions virtuelles
  • Utiliser la liste des plateformes prises en charge pour supprimer le code destiné à des plateformes non prises en charge
    • Du vieux code de support Solaris dans un projet qui ne tournait en pratique que sur FreeBSD
    • Du code avec son propre générateur aléatoire alors que les plateformes réelles disposent toujours d’un RNG
    • Du code de compatibilité pour l’absence de POSIX 2001 alors que l’application ne s’exécute que sur Linux et macOS modernes
    • Du code qui détecte les CPU big-endian pour faire des byte swaps
    • Du code ajouté il y a des années mais qui n’a jamais débouché sur une fonctionnalité réelle
  • De grosses PR de suppression de code peuvent montrer à la fois une baisse du temps de build et une baisse du coût de maintenance

Ajouter linter, formateur et sanitizers

  • Il ne faut pas activer trop de règles de linter d’un coup, mais intégrer quelques bases au cycle de vie du développement
    • clang-tidy et cppcheck peuvent être utiles, mais aussi lents et bruyants
    • Ne pas avoir de linter du tout n’est pas une option, et la première exécution permet souvent de trouver beaucoup de vrais problèmes que les warnings du compilateur ne détectaient pas
  • Le formatage du code doit être appliqué en une seule fois, au bon moment
    • Il faut attendre qu’il n’y ait pas de branche active pour éviter des conflits de fusion catastrophiques
    • Mieux vaut ne pas perdre de temps dans des débats de style et formatter toute la base de code sans exception avec un outil comme clang-format
    • La configuration doit être commitée en même temps
  • Les sanitizers sont nécessaires pour repérer des bugs difficiles à trouver qui peuvent affecter la production
    • La valeur par défaut recommandée est -fsanitize=address,undefined
    • En général, il n’y a pas de faux positifs, donc si quelque chose est détecté, il faut le corriger
    • Les tests doivent aussi être exécutés avec les sanitizers activés
    • Si le budget de performance le permet, on peut envisager d’exécuter une partie de la production avec certains sanitizers activés
  • Même si le compilateur utilisé pour la livraison ne prend pas en charge les sanitizers, on peut utiliser un compilateur comme clang pour le développement et les tests
  • Activer les sanitizers peut révéler des bugs et des fuites mémoire cachés depuis longtemps, et leur correction peut demander beaucoup de travail et de refactorisation
  • Si possible, il vaut mieux aussi compiler les dépendances tierces avec les sanitizers pendant les tests, afin de trouver également des problèmes à l’intérieur des dépendances

Automatiser avec la CI

  • La CI automatise dans un environnement propre les linters, le formatage, les tests et le reste de ce qui a été mis en place
  • Il faut pouvoir produire le binaire de production à chaque changement
  • La plupart des systèmes de CI prennent en charge des matrices multi-plateformes, ce qui permet de vérifier que la liste des plateformes du README build réellement
  • Un pipeline typique peut être aussi simple que make all test lint fmt
  • Les problèmes signalés par les linters et les sanitizers doivent faire échouer le pipeline ; sinon, personne ne les corrigera

Simplifier progressivement le code

  • Une fois qu’une boucle stable de build, de test et de validation existe, on peut commencer à simplifier le code petit à petit
  • On a vu des cas où une classe complexe ne faisait en pratique qu’allouer un pointeur et vérifier s’il est nul, et pouvait donc être remplacée par un booléen
  • Cette étape est difficile à borner dans le temps, car plus on simplifie, plus on découvre d’autres simplifications possibles ; il faut donc rester prudent
  • L’objectif doit être une valeur concrète comme la sécurité, la justesse ou les performances, et non un critère subjectif comme le « clean code »
  • La mise à niveau du standard C++ n’est pas un objectif, mais un moyen
    • Par exemple, cela peut aider à remplacer des incréments manuels d’itérateur par une boucle for (auto x : items)
    • Si le seul besoin est std::clamp, l’écrire soi-même peut être préférable

Une réécriture dans un langage à mémoire sûre demande une décision distincte

  • Réécrire une partie dans un langage à mémoire sûre est une option possible, mais elle s’accompagne de nombreuses précautions
  • Il ne faut s’y engager qu’en présence de raisons fortes

Pour les dépendances, privilégier un modèle centré sur le build depuis les sources

  • C++ ne dispose pas d’une gestion des dépendances cohérente, et beaucoup de projets utilisent le gestionnaire de paquets système
  • Problèmes d’une approche fondée sur les paquets système
    • Les instructions d’installation dépendent de l’OS, de la distribution et de la version de la distribution
    • En passant d’Ubuntu 20.04 à 22.04, on peut se retrouver à devoir mettre à jour 100 dépendances d’un coup à cause d’un changement de version des paquets
    • Les dépendances tierces absentes des paquets doivent de toute façon être buildées depuis les sources
    • Les paquets ne sont pas forcément buildés avec les flags souhaités
      • Les sanitizers, LTO, -march, les informations de debug, le frame pointer ou les différences d’ABI C++ peuvent poser problème
    • Il est difficile de consulter le code source exact de la version utilisée lors des audits, du développement ou du debug
    • Il est difficile de patcher puis rebuild une dépendance
    • Il est difficile d’utiliser exactement les mêmes versions de paquets sur macOS, Ubuntu et FreeBSD
    • Générer automatiquement un BOM devient plus compliqué
    • Il peut manquer la variante de paquet nécessaire, par exemple en bibliothèque statique ou dynamique
  • Des gestionnaires de paquets C++ comme Conan ou vcpkg peuvent constituer une amélioration, mais avec des limites
    • La CI peut devenir complexe et lente à cause des dépendances externes
    • Toutes les versions de paquets ne sont pas forcément disponibles
    • Le système peut ne pas prendre en charge l’OS ou l’architecture nécessaires
  • L’approche recommandée consiste à récupérer les dépendances via des git submodules et à les compiler depuis les sources
    • C’est simple
    • Cela permet d’utiliser l’historique git et les diff au lieu d’un vendor manuel
    • On connaît exactement la version d’une dépendance au commit près
    • Une mise à niveau d’une dépendance unique peut se faire avec git checkout
    • Cela fonctionne sur toutes les plateformes
    • On peut choisir les flags de compilation et le compilateur pour chaque dépendance
    • Même sans expérience C++, les développeurs connaissent git
    • Cela fonctionne de manière récursive
  • On peut builder chaque submodule avec add_subdirectory de CMake ou git submodule foreach make
  • Si les submodules sont trop contraignants, une approche à la Neovim, avec un script unique qui récupère et build les dépendances, est également possible
  • Si le graphe de dépendances est très grand, il peut devenir nécessaire d’utiliser un système de build hybride local/distants comme Buck2 et de réutiliser les artefacts de build
  • Les gestionnaires de paquets de langages compilés comme Go ou Rust adoptent eux aussi une approche fondée sur la compilation depuis les sources

Points renforcés par les suggestions des lecteurs

  • Il faut encore plus insister sur les tests, et une suite de tests C++ doit être exécutée sous sanitizers pour éviter un faux sentiment de sécurité
  • vcpkg peut être un meilleur choix que les git submodules si les exigences et la cross-compilation correspondent à vos besoins
  • Nix peut jouer le rôle de gestionnaire de dépendances C++, mais sa complexité et sa lenteur posent problème
  • Si vous ne corrigez qu’un bug par an, investir dans une grosse refactorisation relève d’un arbitrage ; en revanche, supprimer le code mort et ajouter des sanitizers reste utile même avec des modifications rares
  • La suppression de code doit se concentrer sur les cas où l’analyse statique montre qu’il n’y a pas d’appel ; en cas de doute, mieux vaut ne pas supprimer
    • Les méthodes virtuelles ont leur cible d’appel déterminée à l’exécution, donc elles sont difficiles à supprimer via une simple analyse statique
    • Une conversation de 15 minutes avec les ventes, les product managers ou les utilisateurs pour savoir si une fonctionnalité ou une plateforme est vraiment utilisée peut réduire fortement le travail technique
  • Mettre du code dans un LLM et lui poser des questions peut être juridiquement acceptable ; si possible, il vaut mieux l’exécuter localement et traiter les résultats avec prudence
  • Il existe aussi l’idée d’utiliser des outils d’analyse de code pour générer des diagrammes et des relations entre classes afin de comprendre la structure d’ensemble
  • S’il n’y a pas de système de contrôle de version, l’étape 0 consiste à mettre le code dans un VCS
  • Certains considèrent que la CI doit être l’étape 1 ; c’est un point de vue défendable même si le local reste plus rapide
  • Supprimer des plateformes presque jamais utilisées peut réduire la complexité combinatoire et permettre de grandes simplifications
  • Des builds entièrement reproductibles ne sont peut-être pas réalistes pour une base de code C++ classique, mais des builds fiables le sont
  • Pour un commit de formatage global, git peut être configuré pour ignorer ce commit dans le blame, afin de réduire le coût de suivi de l’historique
  • On peut aussi utiliser les statistiques de l’historique VCS pour repérer les zones souvent modifiées et les fichiers qui changent ensemble
  • Cette approche s’applique non seulement à C++, mais aussi à d’autres bases de code legacy, en dehors des parties spécifiques à C++ comme les sanitizers
  • Working effectively with Legacy Code est recommandé comme livre contenant des conseils utiles
  • Il faut se concentrer sur la valeur réelle, mais sous l’angle de la sécurité, une grande base de code C++ peut contenir de nombreuses vulnérabilités ; la valeur n’est alors pas un gain financier, mais une réduction du risque

1 commentaires

 
GN⁺ 2024-03-01
Avis sur Hacker News
  • Il y a de bons conseils, et d’autres un peu plus controversés. Après avoir hérité de plusieurs énormes projets C++, quelques travaux semblent clairement très rentables au début
    Commencer par mettre en place des builds reproductibles, et encapsuler l’environnement de build avec Docker ou l’outil de packaging de votre choix afin de rendre les outils et dépendances explicites et reproductibles
    Faire en sorte que le projet compile sans avertissements avec -Wall fait ressortir le mauvais code, les comportements indéfinis et les bugs, et permet ensuite de voir immédiatement les avertissements quand j’introduis du code suspect
    Vérifier tôt les erreurs de lecture/écriture avec des outils comme valgrind est aussi un gain facile côté stabilité, et tant qu’on ne comprend pas l’architecture globale, il est plus sûr de garder les refactorings localisés

    • Si vous voulez de l’explicite et du reproductible, mieux vaut éviter de commencer par Docker. Sans beaucoup d’attention, Docker n’offre que la forme la plus diluée de reproductibilité, et peut même donner une fausse impression de sécurité
      Les Dockerfile qui pointent vers des tags d’images changeants sans hash d’intégrité, ou qui appellent apt-get, sont courants, et dans les deux cas ils laissent largement la porte ouverte à des états finaux subtilement différents
      Des solutions comme Bazel ou Nix sont plus difficiles à apprendre, mais fournissent une base bien meilleure
    • L’étape 0, ce sont les builds reproductibles, et l’étape 1 consiste à lancer tous les tests pour signaler les tests instables
      L’étape 2 consiste à lancer tous les tests sous sanitizers et à signaler ceux qui échouent, l’étape 3 à corriger tous ces échecs sous sanitizers, et l’étape 4 correspond au reste du travail
    • Docker convient aux microservices, mais on ne peut pas toujours tout déployer avec Docker. C’est particulièrement vrai pour les gens qui veulent utiliser une application dans Docker, et le Docker-in-Docker est une situation qui ne devrait pas exister
      Les conteneurs sont utiles, mais c’est une très mauvaise façon de faire comme si les problèmes n’existaient pas
      Il faut regrouper toutes les dépendances et éviter un état où le logiciel ne fonctionne que si des milliards de choses sont installées dans /usr/lib dans les bonnes versions
    • Compiler proprement avec -Wall, c’est très bien, mais je suis fermement opposé à mettre -Wall -Werror dans les builds de production
      Certains avertissements du compilateur relèvent presque de l’opinion, et si une nouvelle version du compilateur ajoute un nouvel avertissement, du code qui était propre jusque-là peut soudain être rejeté
      Si -Werror est nécessaire, il vaut mieux ne l’utiliser que dans les builds de debug
  • J’inverserais l’ordre des points 2 et 3. Mettre d’abord en place la CI, le linting, le formatage automatique, etc., doit passer avant le fait d’arracher des choses
    Parce qu’on ne sait pas encore quoi supprimer, ni quelles conséquences cela aura
    Les linters et les outils d’analyse statique donnent beaucoup d’indications sur les parties du programme qui demandent du travail
    Les zones signalées aujourd’hui par les outils d’analyse statique sont souvent celles où l’on pourra plus tard supprimer des fonctions, des classes ou des fichiers entiers qui réimplémentent des concepts de la STL
    Une bibliothèque d’itérateurs maison, des smart pointers maison, l’usage de fonctions de chaînes C, etc., peuvent être remplacés par des algorithmes STL, de vrais smart pointers et les classes de chaînes C++
    Mais tant qu’on n’a pas scanné le code, cela ne se voit pas bien, et il est aussi difficile d’évaluer le résultat avant de pousser vers des builds de test rapides ou vers le déploiement

    • J’ai pas mal d’expérience en agence à intervenir pour sauver des codebases, et la première chose que je fais est de lancer les tests unitaires et de vérifier la couverture
      Là où il manque quelque chose, j’ajoute des smoke tests de base
      Ce travail ne ralentit pas, il accélère au contraire. Une fois une couverture correcte obtenue, on peut avancer beaucoup plus vite lors du refactoring, donc le retour sur ce petit investissement est important
    • À l’inverse, le formatage automatique abîme l’historique des versions et rend plus difficile l’analyse de la date et de la raison pour lesquelles une ligne a été ajoutée
    • Je déconseille absolument d’appliquer du formatage automatique à une codebase legacy
      Les gros projets C++ contiennent généralement non seulement des scripts de génération de code, mais aussi des scripts qui parsèrent le code pour collecter des données destinées à cette génération
      Le formatage automatique peut les casser, et j’ai même vu des projets maudits où des utilisateurs parsèrent des fichiers d’en-tête publics avec des scripts fragiles
    • Ici, la CI est d’une autre nature que les autres éléments. La première étape consiste au minimum à mettre en place un harnais de tests sur le chemin nominal qui s’exécute avec des résultats reproductibles et à chaque commit, ce qui aide aussi à comprendre la codebase
      Il est très probable qu’on doive toucher à beaucoup d’endroits, et quand de nombreuses modifications s’accumulent en local, on peut facilement créer par erreur des commits mélangeant plusieurs sujets
      Dans ces moments-là, la CI devient une sauveuse
    • Plus précisément, dans l’article d’origine, le périmètre de ce qu’il conseille d’arracher est limité au code mort guidé par les avertissements du compilateur et les architectures non prises en charge
      Si vous suivez cette voie, je recommande au minimum de ne pas supprimer directement, mais de commenter pour garder le diff simple jusqu’à ce que la branche soit prête à être squashée puis fusionnée
  • Il manque les outils et techniques de compréhension du code. J’utilisais autrefois un outil écrit en Tcl/Tk appelé Source Navigator, qui était excellent pour indexer une codebase
    Il permettait de consulter la hiérarchie d’appels de la méthode courante et de s’en servir pour produire des diagrammes de séquence UML
    Il existe aussi Source Insight, un outil similaire mentionné ci-dessous
    Et les notes sont importantes. L’essentiel est d’écrire comme si l’on enseignait à quelqu’un
    Au fil des années, je suis devenu plutôt bon pour comprendre du code, et à une époque j’assurais presque seul le support actif et le développement d’une codebase Java de trading algorithmique qui traitait environ 200 millions de dollars par jour sur 4 à 5 places de marché
    Il y avait 35 Mo de documentation sur ce code, et si l’on met de côté le risque lié à la personne clé, le niveau de responsabilité était appréciable
    Franchement, la plupart des grandes codebases contiennent beaucoup de sur-ingénierie et de duplication
    [1] References in "Source Insight" https://d4.alternativeto.net/6S4rr6_0rutCUWnpHNhVq7HMs8GTBs6...

    • Je n’arrive pas à croire que je croise quelqu’un qui a vraiment utilisé Source Navigator dans la nature
      Dans mon cours d’architecture des ordinateurs à l’université, on nous avait forcés à utiliser cette relique, parce qu’elle avait un ensemble de fonctionnalités étrange qu’on ne trouvait pas ailleurs, ainsi que la prise en charge d’un émulateur ARM
      Nous l’utilisions pour programmer en assembleur ARM bare metal
  • Au passage disant que mettre std::cmake dans la bibliothèque standard changerait la donne, j’ai eu l’impression que ça me retournait l’estomac
    Je comprends le conseil qui consiste à arracher à la tronçonneuse tout ce qui n’est pas absolument nécessaire pour fournir les fonctionnalités que l’entreprise ou le projet open source met en avant et vend, mais c’est extrêmement dangereux
    C’est fondamentalement un problème de barrière de Chesterton : si l’on ne comprend pas complètement pourquoi quelque chose s’est retrouvé dans le logiciel et comment le logiciel est utilisé aujourd’hui, on ne peut pas le supprimer
    Dans le pire des cas, au moment de publier environ un mois plus tard, les utilisateurs découvrent qu’une fonctionnalité importante est subtilement cassée, et l’on passe des jours à retracer exactement comment elle l’est
    Ajouter de la CI, des linters, du fuzzing, du formatage automatique, etc. est une bonne idée, mais c’est aussi difficile
    Si l’un utilise principalement VIM, un autre emacs, un autre QTCreator et un autre VSCode, il est très difficile de mettre tout le monde au diapason
    Si l’installation de nouveaux outils est une étape facultative, elle ne se produira pas en pratique, et un linter n’aide pas non plus si, dès l’ouverture du projet, il affiche plus de 2 000 avertissements

    • Ce que l’auteur énumère n’est généralement pas une intégration IDE. Dans les environnements de développement C++, je n’ai jamais vu cpplint, clang-tidy ou un fuzzer intégrés à l’IDE, et ils sont trop lents pour être exécutés automatiquement à chaque frappe
      Seul le formatage automatique est parfois intégré
      Ces choses peuvent être faites en ligne de commande, indépendamment de l’environnement de développement de chacun ; il ne faut donc pas abandonner d’emblée au motif que deux éditeurs de texte sont utilisés, mais au moins essayer
      En C++, si l’équipe refuse d’installer le moindre outil, elle va au-devant de grosses difficultés. Il peut aussi valoir la peine d’envisager de conteneuriser les outils pour faciliter les choses
    • Je pense que le fait que l’un utilise VIM, un autre emacs, QTCreator ou VSCode n’est plus une réponse acceptable
      Nous devons travailler en professionnels, et si le travail exige de mettre en place un IDE et une chaîne d’outils, il faut apprendre à s’en servir
      Le fait que ça compile et fonctionne sur mon ordinateur, dans mon IDE, à ma manière préférée, relève davantage de l’artisanat que du logiciel
    • On peut faire du formatage automatique en dehors de l’éditeur. Il suffit de configurer pre-commit et de l’exécuter dans la CI ; c’est anodin
      Même si l’installation du hook local est facultative, si les PR échouent dans la CI, les gens finiront par le faire
      Ce sont vraiment des bases, mais l’écart de connaissances sur la bonne gestion de la CI et de l’infrastructure semble assez important
    • Vous semblez avoir raté la note disant qu’on peut tout simplement refuser les exigences de base de son patron. Celui qui vous verse votre salaire, donc
    • Les hooks pre-commit facultatifs en local doivent être soutenus par des étapes obligatoires dans la CI
      Il faut permettre aux développeurs d’exécuter localement les tests, le lint, le fuzzing, le formatage, la validation du format YAML, la vérification de l’absence de saut de ligne en fin de fichier, etc., afin de les aider à éviter un échec de la CI avant de push
      Si un linter émet des milliers d’avertissements à l’ouverture du projet, le développeur qui l’ajoute doit faire en sorte qu’il y ait 0 avertissement avant de merger ce changement
      On peut y parvenir en désactivant certains avertissements ou certains fichiers, en corrigeant une partie des problèmes, ou en combinant ces approches
  • À mon avis, la première étape consiste à contacter l’ancien mainteneur, à aller le voir, à lui offrir un thé ou une bière, et à finir par parler de la codebase
    Les magiciens que sont les anciens mainteneurs peuvent vous apprendre beaucoup de choses
    Les autres suggestions, comme faire tourner le logiciel sur plusieurs plateformes ou faire passer les tests, me semblent être de bons stress tests menant à plus de robustesse et de compréhension
    Mais je commencerais quand même par cueillir ce fruit facile : parler aux personnes qui sont passées avant

    • Je n’en ferais pas la première étape. On risque alors de leur faire perdre leur temps
      Il vaut mieux commencer par travailler un peu soi-même, se retrouver bloqué à plusieurs endroits, puis parler à l’ancien mainteneur : ce sera bien plus productif
      Ils apprécieront aussi cet effort
    • Il peut être préférable de parcourir rapidement la codebase pour identifier les plus gros WTF, puis de poser des questions
      Si vous héritez d’une codebase sans tests, dont le build échoue une fois sur deux, avec des informations de dépendances peu claires, et qui ne compile que sur un unique serveur équipé d’un OS très ancien, vous ne pouvez pas être certain que l’ancien mainteneur soit vraiment un magicien
      Il faut aussi se demander si tous les problèmes viennent d’un manque de temps, ou si ce « magicien » l’a maintenue cassée pour assurer sa sécurité de l’emploi, ou parce qu’il ne voulait pas apprendre de nouvelles choses
    • Si vous avez déjà interrogé un ancien mainteneur sur du code legacy, vous le savez : même si la passation a eu lieu hier, il pourra rarement vous dire quoi que ce soit d’utile sur quelque chose fait il y a plus de 6 mois
      C’est le meilleur des cas ; le cas courant, c’est que la réponse à toutes les questions soit : « c’était déjà comme ça quand j’en ai hérité »
    • J’ai déjà été chargé de déployer un logiciel sur un vieil OS personnalisé dans un réseau militaire isolé. Ce n’était pas un gros programme, environ 50 000 lignes
      Pendant le travail, je suis tombé sur plusieurs bugs et problèmes, et j’ai essayé de contacter les développeurs qui avaient écrit ce logiciel sur mesure pour mon employeur
      Il s’est avéré qu’il avait été écrit par un seul sous-traitant, décédé quelques années plus tôt
      Dans l’industrie de la défense, ce genre de situation est courant. Il y a beaucoup d’objets sur mesure et jetables, conçus pour un système particulier, et surtout côté matériel, il n’est pas rare que les ingénieurs qui ont conçu l’équipement soient partis ou à la retraite depuis longtemps
    • C’est plus facile à dire qu’à faire. Mon étape 0 consiste à le mettre dans un outil UML pour en extraire des diagrammes de classes et d’autres diagrammes
      Ça aide beaucoup
      Une codebase C++ avec des tests, c’est optimiste à mon avis
  • Je ne comprends pas vraiment pourquoi il y a autant d’insistance sur le refactoring ou l’amélioration. Si on peut ajouter une fonctionnalité en la greffant au code, il suffit de faire ça sans toucher au reste
    Si le changement est suffisamment important, on peut extraire ce dont on a besoin du code legacy via un appel à une fonction externe, l’introduction d’une couche réseau, ou la séparation du code commun dans une bibliothèque, puis faire le reste dans un nouvel environnement
    Je ne tenterais pas de gros refactoring, sauf si plusieurs personnes doivent travailler sur ce code à l’avenir et qu’il faut certaines hypothèses et standards pour que le groupe puisse collaborer facilement

    • Le texte original s’oppose aux gros refactorings. Les travaux incrémentaux qu’il propose vont dans le sens de rendre le code progressivement plus facile à manipuler
      L’approche qui consiste à ajouter par-dessus fonctionne jusqu’à un certain point, mais il arrive un moment où, après une modification, quelque chose casse soudainement et le désordre est tel qu’on met bien trop longtemps à en trouver la cause
      Le propos de l’article est plutôt d’éviter les grandes réécritures, tout en faisant du rangement pour maintenir la cohésion et faciliter les changements
    • La bonne réponse dépend de l’avenir. J’ai déjà travaillé sur du code C++ dont un remplaçant existait déjà sur le marché, mais dont il fallait encore publier quelques versions de l’ancien code
      Parfois, il fallait ajouter la même fonctionnalité dans les deux versions
      On ne traite pas du tout de la même façon un code dont on sait que la dernière release arrive bientôt et un code qu’il faudra maintenir et enrichir pendant encore des décennies
    • Dans quelques cas chanceux, on peut simplement greffer une fonctionnalité
      En pratique, une nouvelle fonctionnalité est souvent une modification d’un comportement déjà existant, et on se retrouve soudain à devoir faire un gros refactoring à plusieurs endroits
  • C’est un fil avec beaucoup de bons conseils. Pour ajouter quelque chose qui ne se limite pas à C/C++, si vous avez la possibilité d’utiliser un système de gestion de versions, il faut en exploiter pleinement la valeur
    Beaucoup d’équipes s’en servent seulement comme outil de collaboration, mais il permet de faire bien plus
    Il suffit de récupérer l’historique et d’en faire une petite base de données. Pas besoin que ce soit une base relationnelle : on peut commencer avec des fichiers JSON ou une feuille de calcul
    Rien qu’avec une approche guidée par les données, on peut presque immédiatement extraire beaucoup d’informations utiles
    Les fichiers et fonctions souvent modifiés seront probablement des hotspots des prochains travaux ; si l’on veut introduire des tests unitaires ou réduire les conflits de merge, il faut donc se concentrer dessus
    Si des fichiers qui semblent éloignés changent souvent ensemble, cela peut indiquer une structure implicite qui ne se voit pas à la seule lecture du code
    Le modèle réel de propriété de chaque module peut aussi être déduit de l’historique, et une propriété floue peut être un signal qu’un refactoring est nécessaire
    En C/C++, l’amélioration des temps de build doit elle aussi se concentrer, à partir des données, sur les modules importants. Plutôt que de supprimer au hasard des dépendances de fichiers, on peut découper les modules qui changent souvent et même les combiner aux dépendances de headers pour attribuer un score à leur impact réel sur le temps de build
    En intégrant le VCS à d’autres outils de développement, on peut faire encore davantage ; à l’ère des LLM, on pourrait aussi injecter l’historique du projet et ses métadonnées dans un modèle pour lui demander des éclairages intéressants
    Sans énorme fenêtre de contexte, cela demanderait peut-être une ingénierie de modèle dédiée, mais j’ai l’intuition que ça vaut la peine d’essayer

    • Je serais curieux d’avoir des conseils de logiciels aidant à automatiser ce type d’analyse
  • Le conseil d’ajouter de la CI, des linters, du fuzzing, du formatage automatique, etc., mérite d’être davantage découpé
    La CI doit garantir que le projet build ailleurs que sur ma machine, afin d’éviter les régressions liées à la compilation
    Les avertissements du compilateur et les analyseurs statiques sont généralement plus intelligents que moi ; donc s’ils signalent quelque chose d’inquiétant parce que je fais des choses bizarres avec des pointeurs, c’est un signal fort à vérifier
    Les tests unitaires doivent vérifier que le code important fait exactement ce qu’on attend de lui à bas niveau, et comme il y a de fortes chances que ce ne soit pas le cas, il faut comprendre pourquoi
    Corriger quelque chose peut en faire exploser une autre, car le code existant a peut-être été écrit en supposant un comportement bogué
    Le formatage automatique n’est pas une priorité, et je pense qu’il vaut mieux suivre le style des mainteneurs existants
    L’idée que la dernière étape d’une base de code C++ héritée soit une réécriture dans un langage à sûreté mémoire a aussi de grandes chances de mal s’appliquer
    Il est difficile d’obtenir des ressources de travail supplémentaires pour quelque chose qui n’est pas cassé ; il faut alors connaître un langage de plus en plus du C++, et les tests peuvent devenir plus complexes
    Les contraintes de mémoire ou de performance peuvent aussi rendre peu adaptée une écriture en plusieurs langages ; et, au départ, hériter d’une base de code legacy revient presque à reconnaître qu’on manquait de ressources — temps, argent, connaissances — pour la réécrire

    • En réalité, on peut atteindre cet objectif au sein d’une base de code C++ simplement en éliminant le C++ non sûr vis-à-vis de la mémoire et en imposant des règles
      « Réécrire en X » ajoute seulement de la complexité parce que c’est à la mode
      Si l’on est déjà en train de réécrire une grande partie de la base de code en C++, mieux vaut suivre un sous-ensemble plus restrictif de C++, et High Integrity C++ me semble correct
      Si l’on peut se procurer la norme MISRA récente, elle sera probablement très bien aussi
      Plutôt que de faire réapprendre à toute l’équipe les angles dangereux d’un nouveau langage, mieux vaut utiliser un langage qu’elle connaît déjà tout en imposant des règles pour éviter ses pièges connus
  • Il est étrange que l’auteur critique longuement l’automatisation de la BOM, la gestion des versions de paquets, la provenance des dépendances, etc., puis suggère que les git submodules sont préférables à un gestionnaire de paquets
    Avant de formuler ce genre de critique, il faudrait essayer vcpkg
    Il y a bien quelques aspérités, mais presque tout est couvert de façon intuitive par vcpkg
    La mise à jour des dépendances est un peu plus difficile qu’avec des git submodules, mais j’y vois plutôt une fonctionnalité qu’un bug. Les dépendances sont buildées dans des bacs à sable séparés, puis installées dans un répertoire spécifié
    vcpkg permet de conserver un caractère vendored-in en configurant un dépôt interne comme registre au lieu du dépôt officiel, de chaîner la toolchain pour que tout soit compilé avec un jeu de flags fixe, et autorise aussi les personnalisations par port
    C’est parce que ce type d’abstraction est utile que les gestionnaires de paquets sont populaires, et que tout le monde n’a pas à manipuler directement des chaînes interminables de flags de compilation, de macros, d’avertissements, etc.

  • L’article était intéressant et j’y ai appris des choses. Je me demandais toutefois à quels langages les gens font référence quand ils parlent de « réécrire dans un langage sûr pour la mémoire »
    Veulent-ils dire réécrire certaines parties en Go, Java ou C#, ou bien est-ce une pique du genre « réécrivons en Rust », avec une part de dénégation possible ?

    • En tant qu’auteur, je traiterai ce sujet dans le deuxième article
      La conclusion dépend entièrement de l’équipe et des contraintes. Par exemple, il faut savoir si le garbage collection est envisageable, et si oui, si Go est un bon choix, si la sécurité est la priorité absolue, etc.
      La plupart des développeurs C++ peuvent généralement utiliser Rust assez facilement et obtenir des performances équivalentes, à mon avis
      Mais il y a aussi des cas où il n’y avait pas vraiment de bonne raison que le projet soit en C++ au départ, et j’ai vu des réécritures réussies en Java
      Apple réécrit une partie de son code C++ en Swift
      Une bonne règle empirique est de choisir un langage avec lequel l’équipe ou l’entreprise est à l’aise
    • C’était un article sur C++, le mot « Rust » n’y apparaissait pas une seule fois, et il ne mentionnait que les langages « memory safe », dont il existe des dizaines, mais il est intéressant de voir comment on y a quand même glissé le mème Rust pour le dénigrer
      On en est donc au stade où l’on soupçonne désormais l’auteur d’être un programmeur Rust maléfique et crypté. Sans doute parce que Rust n’était pas mentionné au départ, donc qu’il n’était pas possible de s’en plaindre directement