Estás leyendo la documentación para una versión de desarrollo. Para la última versión publicada, por favor revisa Humble.

Guía de calidad: garantizar la calidad del código

This page gives guidance about how to improve the software quality of ROS 2 packages, focusing on more specific areas than the Quality Practices section of the Developer Guide.

Las siguientes secciones pretenden abordar los paquetes básicos, de aplicaciones y de ecosistemas de ROS 2 y las bibliotecas principales de clientes, C++ y Python. Las soluciones presentadas están motivadas por consideraciones de diseño e implementación para mejorar atributos de calidad como «Confiabilidad», «Seguridad», «Mantenibilidad», «Determinismo», etc. que se relacionan con requisitos no funcionales.

Análisis de código estático como parte de la compilación del paquete ament

Contexto:

  • Has desarrollado tu código de producción C++.

  • Has creado un paquete ROS 2 con soporte de compilación con ament.

Problema:

  • El análisis de código estático a nivel de biblioteca no se ejecuta como parte del procedimiento de creación del paquete.

  • El análisis de código estático a nivel de biblioteca debe ejecutarse manualmente.

  • Riesgo de olvidarse de ejecutar el análisis de código estático a nivel de biblioteca antes de construir una nueva versión del paquete.

Solución:

  • Usa las capacidades de integración de ament para ejecutar análisis de código estático como parte del procedimiento de construcción del paquete.

Implementación:

  • Insert into the packages CMakeLists.txt file.

...
if(BUILD_TESTING)
  find_package(ament_lint_auto REQUIRED)
  ament_lint_auto_find_test_dependencies()
  ...
endif()
...
  • Inserta las dependencias de prueba ament_lint en el archivo package.xml de los paquetes.

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

Ejemplos:

Contexto resultante:

  • Las herramientas de análisis de código estático soportadas por ament se ejecutan como parte de la construcción del paquete.

  • Las herramientas de análisis de código estático que no son compatibles con ament deben ejecutarse por separado.

Análisis estático de seguridad de hilos a través de la anotación de código

Contexto:

  • Estás desarrollando/depurando tu código de producción C++ multihilo

  • Accedes a datos de múltiples hilos en código C++

Problema:

  • Las carreras de datos y los interbloqueos pueden provocar errores críticos.

Solución:

Contexto para la implementación:

Para habilitar el Análisis de Seguridad de hilos, se debe anotar el código para que el compilador sepa más sobre la semántica del código. Estas anotaciones son atributos específicos de Clang, p. __atributo__(capacidad())). En lugar de usar esos atributos directamente, ROS 2 proporciona macros de preprocesador que se borran cuando se usan otros compiladores.

Estas macros se pueden encontrar en rcpputils/thread_safety_annotations.hpp

La documentación del análisis de seguridad de hilos establece

El análisis de seguridad de hilos se puede usar con cualquier biblioteca de hilos, pero requiere que la API de hilos se incluya en clases y métodos que tengan las anotaciones adecuadas.

Hemos decidido que queremos que los desarrolladores de ROS 2 puedan usar primitivas de subprocesamiento std:: directamente para su desarrollo. No queremos proporcionar nuestros propios tipos envueltos como se sugiere anteriormente.

Hay tres bibliotecas estándar de C++ a tener en cuenta

  • La biblioteca estándar GNU libstdc++: predeterminada en Linux, explícitamente a través de la opción del compilador -stdlib=libstdc++

  • La biblioteca estándar LLVM libc++ (también llamada libcxx) - predeterminada en macOS, establecida explícitamente por la opción del compilador -stdlib=libc++

  • La biblioteca estándar de Windows C++: no es relevante para este caso de uso

libcxx anota sus implementaciones std::mutex y std::lock_guard para el análisis de seguridad de hilos. Cuando se usa GNU libstdc++ , esas anotaciones no están presentes, por lo que el análisis de seguridad de hilos no se puede usar en tipos std:: no encapsulados.

Por lo tanto, para usar Thread Safety Analysis directamente con tipos std:: , debemos usar libcxx

Implementación:

Las sugerencias de migración de código aquí de ninguna manera están completas - al escribir (o anotar las existentes) código multihilo, se te recomienda a utilizar tantas anotaciones como sea lógico para tu caso de uso. Sin embargo, ¡este paso a paso es un gran lugar para comenzar!

  • Habilitación de análisis para paquete/objetivo

    Cuando el compilador de C++ es Clang, habilite el indicador -Wthread-safety. Ejemplo a continuación para proyectos basados en 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()
    
  • Código de anotación

    • Paso 1 - Anotar miembros de datos

      • Encuentre en cualquier lugar que std::mutex se use para proteger algunos datos de miembros

      • Agrega la anotación RCPPUTILS_TSA_GUARDED_BY(mutex_name) a los datos que están protegidos por el 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;
    };
    
    • Paso 2: corregir advertencias

      • En el ejemplo anterior, Foo::get producirá una advertencia del compilador. Para solucionarlo, bloquear antes de volver a la bar

    void get() const {
      std::lock_guard<std::mutex> lock(mutex_);
      return bar;
    }
    
    • Paso 3 - (Opcional pero recomendado) Refactorizar el código existente a un patrón privado-Mutex

      Un patrón recomendado en código C++ multihilo es mantener siempre su mutex como miembro privado: de la estructura de datos. Esto hace que la seguridad de los datos sea la preocupación de la estructura contenedora, descargando esa responsabilidad de los usuarios de la estructura y minimizando el área superficial del código afectado.

      Hacer que sus bloqueos sean privados puede requerir repensar las interfaces de sus datos. Este es un gran ejercicio - aquí hay algunas cosas a considerar

      • Es posible que desees proporcionar interfaces especializadas para realizar análisis que requieran una lógica de bloqueo compleja, p. contar miembros en un conjunto filtrado de una estructura de mapa protegida por mutex, en lugar de devolver la estructura subyacente a los consumidores

      • Considera la posibilidad de copiar para evitar el bloqueo, donde la cantidad de datos es pequeña. Esto puede permitir que otros hilos sigan accediendo a los datos compartidos, lo que potencialmente puede conducir a un mejor rendimiento general.

    • Paso 4 - (Opcional) Habilitar análisis de capacidad negativa

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

      El análisis de capacidad negativa te permite especificar «este bloqueo no debe mantenerse al llamar a esta función». Puede revelar posibles casos de punto muerto que otras anotaciones no pueden.

      • Donde especificas -Wthread-safety, agrega el indicador adicional -Wthread-safety-negative

      • En cualquier función que adquiera un bloqueo, a el patrón RCPPUTILS_TSA_REQUIRES(!mutex)

  • Cómo ejecutar el análisis

    • La granja de compilación ROS CI ejecuta un trabajo nocturno con libcxx, que hará surgir cualquier problema en la pila principal de ROS 2 al marcarse como «Inestable» cuando Thread Safety Analysis genere advertencias

    • Para ejecuciones locales, tienes las siguientes opciones, todas equivalentes

      • Utiliza el mixin colcon clang-libcxx

      • Pasar el compilador a 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

      • Anulación del compilador del sistema

        • 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

Contexto resultante:

  • Los interbloqueos potenciales y las condiciones de ejecución aparecerán en el momento de la compilación, al usar Clang y libcxx

Análisis dinámico (ejecución de datos y puntos muertos)

Contexto:

  • Estas desarrollando/depurando tu código de producción C++ multiproceso.

  • Usas pthreads o C++11 threading + llvm libc++ (en el caso de ThreadSanitizer).

  • No utilizas enlaces estáticos Libc/libstdc++ (en el caso de ThreadSanitizer).

  • No creas ejecutables que no sean independientes de la posición (en el caso de ThreadSanitizer).

Problema:

  • Las condiciones de carrera y los interbloqueos pueden provocar errores críticos.

  • Las condiciones de carrerq y los interbloqueos no se pueden detectar mediante el análisis estático (motivo: limitación del análisis estático).

  • Las condiciones de carrera y los interbloqueos no deben aparecer durante la depuración/prueba del desarrollo (razón: por lo general, no se ejercen todas las rutas de control posibles a través del código de producción).

Solución:

  • Utilizar una herramienta de análisis dinámico que se centre en la búsqueda de condiciones de carrera y puntos muertos (aquí clang ThreadSanitizer).

Implementación:

Contexto resultante:

  • Mayor probabilidad de encontrar carreras de datos y puntos muertos en el código de producción antes de implementarlo.

  • El resultado del análisis puede carecer de fiabilidad, herramienta en fase beta (en el caso de ThreadSanitizer).

  • Overhead por instrumentación de código de producción (mantenimiento de ramas separadas para código de producción instrumentado/no instrumentado, etc.).

  • El código instrumentado necesita más memoria por subproceso (en el caso de ThreadSanitizer).

  • El código instrumentado asigna mucho espacio de direcciones virtuales (en el caso de ThreadSanitizer).