Guide qualité : assurer la qualité du code

Cette page donne des conseils sur la façon d’améliorer la qualité logicielle des packages ROS 2, en se concentrant sur des domaines plus spécifiques que la section Pratiques de qualité du Guide du développeur.

Les sections ci-dessous ont pour but d’aborder le noyau ROS 2, les packages d’application et d’écosystème et les bibliothèques clientes principales, C++ et Python. Les solutions présentées sont motivées par des considérations de conception et de mise en œuvre pour améliorer les attributs de qualité tels que « Fiabilité », « Sécurité », « Maintenabilité », « Déterminisme », etc. qui se rapportent à des exigences non fonctionnelles.

Analyse de code statique dans le cadre de la construction du package ament

Contexte:

  • Vous avez développé votre code de production C++.

  • Vous avez créé un package ROS 2 avec prise en charge de la construction avec ament.

Problème:

  • L’analyse de code statique au niveau de la bibliothèque n’est pas exécutée dans le cadre de la procédure de construction du package.

  • L’analyse de code statique au niveau de la bibliothèque doit être exécutée manuellement.

  • Risque d’oublier d’exécuter une analyse de code statique au niveau de la bibliothèque avant de créer une nouvelle version de package.

Solution:

  • Utilisez les capacités d’intégration de ament pour exécuter une analyse de code statique dans le cadre de la procédure de construction du package.

Mise en œuvre:

  • Insérez dans les packages le fichier CMakeLists.txt.

...
if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  ament_lint_auto_find_test_dependencies()
  ...
endif()
...
  • Insérez les dépendances de test ament_lint dans le fichier package.xml des packages.

...
<package format="2">
  ...
  <test_depend>ament_lint_auto</test_depend>
  <test_depend>ament_lint_common</test_depend>
  ...
</package>

Exemples:

Contexte résultant :

  • Les outils d’analyse de code statique pris en charge par ament sont exécutés dans le cadre de la construction du package.

  • Les outils d’analyse de code statique non pris en charge par ament doivent être exécutés séparément.

Analyse statique de la sécurité des threads via l’annotation de code

Contexte:

  • Vous développez/déboguez votre code de production C++ multithread

  • Vous accédez aux données de plusieurs threads dans le code C++

Problème:

  • Les courses aux données et les blocages peuvent entraîner des bogues critiques.

Solution:

Contexte de mise en œuvre :

Pour activer l’analyse de la sécurité des threads, le code doit être annoté pour permettre au compilateur d’en savoir plus sur la sémantique du code. Ces annotations sont des attributs spécifiques à Clang - par ex. __attribut__(capacité())). Au lieu d’utiliser ces attributs directement, ROS 2 fournit des macros de préprocesseur qui sont effacées lors de l’utilisation d’autres compilateurs.

Ces macros se trouvent dans rcpputils/thread_safety_annotations.hpp

La documentation de l’analyse de la sécurité des threads indique

L’analyse de la sécurité des threads peut être utilisée avec n’importe quelle bibliothèque de threads, mais elle nécessite que l’API de threading soit encapsulée dans des classes et des méthodes qui ont les annotations appropriées

Nous avons décidé que nous voulions que les développeurs ROS 2 puissent utiliser les primitives de threading std:: directement pour leur développement. Nous ne voulons pas fournir nos propres types enveloppés comme suggéré ci-dessus.

Il existe trois bibliothèques standard C++ à connaître * La bibliothèque standard GNU libstdc++ - par défaut sous Linux, explicitement via l’option du compilateur -stdlib=libstdc++ * La bibliothèque standard LLVM libc++ (également appelé libcxx ) - par défaut sur macOS, défini explicitement par l’option du compilateur -stdlib=libc++ * La bibliothèque standard C++ de Windows - non pertinente pour ce cas d’utilisation

libcxx annote ses implémentations std::mutex et std::lock_guard pour l’analyse de la sécurité des threads. Lors de l’utilisation de GNU libstdc++ , ces annotations ne sont pas présentes, donc l’analyse de la sécurité des threads ne peut pas être utilisée sur des types std:: non encapsulés.

Par conséquent, pour utiliser Thread Safety Analysis directement avec std:: types, nous devons utiliser libcxx

Mise en œuvre:

Les suggestions de migration de code ici ne sont en aucun cas complètes - lors de l’écriture (ou de l’annotation de code fileté existant), nous vous encourageons à utiliser autant d’annotations que cela est logique pour votre cas d’utilisation. Cependant, cette étape par étape est un excellent point de départ !

  • Activation de l’analyse pour le package/la cible

    Lorsque le compilateur C++ est Clang, activez le drapeau -Wthread-safety. Exemple ci-dessous pour les projets basés sur CMake

    if(CMAKE_CXX_COMPILER_ID MATCHES "Clang")
      add_compile_options(-Wthread-safety)   # for your whole package
      target_compile_options(${MY_TARGET} PUBLIC -Wthread-safety)  # for a single library or executable
    endif()
    
  • Code d’annotation

    • Étape 1 - Annoter les membres de données

      • Trouvez n’importe où std::mutex est utilisé pour protéger certaines données de membre

      • Ajoutez l’annotation RCPPUTILS_TSA_GUARDED_BY(mutex_name) aux données protégées par le mutex

      class Foo {
      public:
        void incr(int amount) {
          std::lock_guard<std::mutex> lock(mutex_);
          bar += amount;
        }
      
        void get() const {
          return bar;
        }
      
      private:
        mutable std::mutex mutex_;
        int bar RCPPUTILS_TSA_GUARDED_BY(mutex_) = 0;
      };
      
    • Étape 2 - Correction des avertissements

      • Dans l’exemple ci-dessus - Foo::get produira un avertissement du compilateur ! Pour y remédier, verrouillez avant de retourner la barre

      void get() const {
        std::lock_guard<std::mutex> lock(mutex_);
        return bar;
      }
      
    • Étape 3 - (Facultatif mais recommandé) Refactoriser le code existant en modèle Private-Mutex

      Un modèle recommandé dans le code C++ threadé est de toujours garder votre mutex en tant que membre private: de la structure de données. Cela fait de la sécurité des données la préoccupation de la structure contenante, déchargeant cette responsabilité des utilisateurs de la structure et minimisant la surface du code affecté.

      Rendre vos serrures privées peut nécessiter de repenser les interfaces avec vos données. C’est un excellent exercice - voici quelques points à considérer

      • Vous souhaiterez peut-être fournir des interfaces spécialisées pour effectuer des analyses nécessitant une logique de verrouillage complexe, par ex. compter les membres dans un ensemble filtré d’une structure de carte mutex-gardée, au lieu de renvoyer réellement la structure sous-jacente aux consommateurs

      • Envisagez de copier pour éviter le blocage, lorsque la quantité de données est faible. Cela peut permettre à d’autres threads d’accéder aux données partagées, ce qui peut potentiellement conduire à de meilleures performances globales.

    • Étape 4 - (Facultatif) Activer l’analyse de capacité négative

      https://clang.llvm.org/docs/ThreadSafetyAnalysis.html#negative-capabilities

      L’analyse de capacité négative vous permet de spécifier « ce verrou ne doit pas être maintenu lors de l’appel de cette fonction ». Il peut révéler des cas de blocage potentiels que d’autres annotations ne peuvent pas.

      • Là où vous avez spécifié -Wthread-safety, ajoutez le drapeau supplémentaire -Wthread-safety-negative

      • Sur toute fonction qui acquiert un verrou, utilisez le motif RCPPUTILS_TSA_REQUIRES(!mutex)

  • Comment exécuter l’analyse

    • La ferme de construction ROS CI exécute un travail nocturne avec `` libcxx``, qui fera apparaître tous les problèmes dans la pile principale ROS 2 en étant marqué « Instable » lorsque l’analyse de la sécurité des threads déclenche des avertissements

    • Pour les exécutions locales, vous disposez des options suivantes, toutes équivalentes

      • Utilisez le mixin colcon clang-libcxx

      • Passer le compilateur à CMake

        • colcon build --cmake-args -DCMAKE_C_COMPILER=clang -DCMAKE_CXX_COMPILER=clang++ -DCMAKE_CXX_FLAGS='-stdlib=libc++ -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS' -DFORCE_BUILD_VENDOR_PKG=ON --no-warn-unused-cli

      • Remplacer le compilateur système

        • CC=clang CXX=clang++ colcon build --cmake-args -DCMAKE_CXX_FLAGS='-stdlib=libc++ -D_LIBCPP_ENABLE_THREAD_SAFETY_ANNOTATIONS' -DFORCE_BUILD_VENDOR_PKG=ON --no-warn-unused-cli

Contexte résultant :

  • Les blocages potentiels et les conditions de concurrence seront signalés au moment de la compilation, lors de l’utilisation de Clang et libcxx

Analyse dynamique (data races & deadlocks)

Contexte:

  • Vous développez/déboguez votre code de production C++ multithread.

  • Vous utilisez pthreads ou threading C++11 + llvm libc++ (dans le cas de ThreadSanitizer).

  • Vous n’utilisez pas la liaison statique Libc/libstdc++ (dans le cas de ThreadSanitizer).

  • Vous ne créez pas d’exécutables non indépendants de la position (dans le cas de ThreadSanitizer).

Problème:

  • Les courses aux données et les blocages peuvent entraîner des bogues critiques.

  • Les courses de données et les blocages ne peuvent pas être détectés à l’aide de l’analyse statique (raison : limitation de l’analyse statique).

  • Les courses de données et les blocages ne doivent pas apparaître pendant le débogage/test de développement (raison : généralement, tous les chemins de contrôle possibles via le code de production ne sont pas exercés).

Solution:

  • Utilisez un outil d’analyse dynamique qui se concentre sur la recherche de courses de données et de blocages (ici clang ThreadSanitizer).

Mise en œuvre:

Contexte résultant :

  • Plus de chances de trouver des courses de données et des blocages dans le code de production avant de le déployer.

  • Le résultat de l’analyse peut manquer de fiabilité, outil en phase bêta (dans le cas de ThreadSanitizer).

  • Surcoût dû à l’instrumentation du code de production (maintien de branches distinctes pour le code de production instrumenté/non instrumenté, etc.).

  • Le code instrumenté nécessite plus de mémoire par thread (dans le cas de ThreadSanitizer).

  • Le code instrumenté mappe beaucoup d’espace d’adressage virtuel (dans le cas de ThreadSanitizer).