Nouveau

Quelques trucs avec CMake

CMake, c’est bien, c’est puissant, c’est utilisé partout sur GitHub. Seulement, passés les tutoriels simples, CMake, c’est un mur pas vraiment évident à escalader. J’ai créé ce blog pour ne pas oublier les trucs et astuces qui pourraient me resservir, alors quoi de plus naturel que de faire un article avec quelques trucs sur CMake ? Parce qu’il y a certains trucs que j’ai mis très longtemps à trouver…

CMake est un outil qui évolue vite. En cherchant comment réaliser une action particulière, on trouve des tas de solutions différentes sur le net et beaucoup sont obsolètes. J’ai essayé de lister ici les techniques modernes mais malheureusement elles seront peut-être obsolètes dans quelques temps… D’ici là, enjoy  : )

Choisir les versions des langages C et C++

La solution simple moderne est de définir les variables dédiées : CMAKE_C_STANDARD et CMAKE_CXX_STANDARD. Par exemple pour utiliser C++11 :

set(CMAKE_CXX_STANDARD 11)

Vous pouvez lire cet article pour plus de détails et de manière de choisir les version des langages : Enabling C++11 And Later In CMake par Craig Scott.

Attention ! Par défaut, GCC compilera avec les extensions ! Si ce n’est pas ce que vos voulez, dites à CMake que vous en voulez pas d’extensions :

set(CMAKE_CXX_EXTENSIONS OFF)

Ajouter des options de compilations

La solution la plus simple et portable est d’utiliser add_compile_options(). Par exemple pour utiliser l’option -Wall :

add_compile_options(-Wall)

-Wall sera ajouté pour le C et le C++, pour tous les cibles, pour toutes les configurations.

Si vous ne souhaitez ajouter cette option qu’à une seule cible, il faut utiliser target_compile_options() à la place. Certains, comme Boost, pensent qu’on ne devrait utiliser que target_compile_options().

Si vous ne souhaitez que cette option que pour le C ou que pour C++, vous pourriez être tentés de modifier directement les variables CMAKE_lang_FLAGS_config, comme par exemple :

set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-rtti -Wsuggest-override")
set(CMAKE_CXX_FLAGS_DEBUG "${CMAKE_CXX_FLAGS_DEBUG} -Og")

Il est toutefois possible d’utiliser une generator expression pour faire ça (et c’est visiblement le style CMake moderne) :

add_compile_options($<$<COMPILE_LANGUAGE:CXX>:-Wsuggest-override>) # uniquement en C++

add_compile_options($<$<CONFIG:Debug>:-Og>) # uniquement en debug

add_compile_options($<$<AND:$<COMPILE_LANGUAGE:CXX>,$<CONFIG:Debug>>:-Og>) # uniquement en C++ en debug

NOTE : Chaque fois que vous devez modifier une variable interne de CMake, pensez à ajouter à sa valeur courante et non pas simplement à l’écraser (voir l’item 1 de cet article) ! Sauf si c’est vraiment ce que vous voulez faire…

NOTE : Chaque fois que vous ajoutez des options spécifiques à un compilateur, n’oubliez pas que vous casser la possible d’utiliser votre projet avec un autre compilateur. Si vous voulez continuer à supporter plusieurs compilateurs, il faudrait tester le compilateur choisi et s’adapter, soit avec des if() / endif() soit directement avec des generator expressions.

Spécifier le type de build

Vous avez un projet CMake et vous faites :

cmake -G "Unix Makefiles" .

Vous obtenez un jeu de makefiles répondant au type de build par défaut, mais lequel ? Debug ou Release ? Et bien, rien… Il n’y a pas de mode par défaut, ce qui signifie que la variable CMAKE_BUILD_TYPE est vide ! Cela implique que les variables comme CMAKE_CXX_FLAGS_DEBUG n’est pas utilisées, qu’une générateur expression comme $<CONFIG:Debug> sera égale à 0, etc. Cela signifie aussi que CMake n’ajoute pas automatiquement des options comme -g ou -O2 pour gcc.

N’oubliez donc pas de choisir !

cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Debug .
cmake -G "Unix Makefiles" -DCMAKE_BUILD_TYPE=Release .

Ajouter des options particulières à certains fichiers ou dossiers

Il est possible d’ajouter des options de compilations pour un fichier en particulier grâce à set_source_files_properties(). C’est très utile pour optimiser un fichier en particulier :

set_source_files_properties(chemin/vers/mon_fichier.cpp PROPERTIES COMPILE_FLAGS -O3)

Attention ! Il s’agit bien d’ajouter une option, pas de remplacer les options déjà appliquées. Si vous voulez par exemple ne plus avoir les warnings générés par -Wconversion, ajoutez l’option -Wno-conversion pour l’annuler.

Il est possible de passer une liste de fichiers et on peut ainsi ruser pour modifier un dossier :

file(GLOB_RECURSE MON_DOSSIER chemin/vers/mon/dossier/*.cpp)
set_source_files_properties(${MON_DOSSIER} PROPERTIES COMPILE_FLAGS -O3)

Afficher la taille de l’exécutable après le build

Vous avez sans doute envie garder un œil sur la taille de votre exécutable. C’est particulièrement le cas si vous travaillez sur un projet embarqué. Une solution simple est d’appeler size (ou un programme équivalent de votre toolchain) grâce à add_custom_command() en tant que post build action. Voici un exemple avec une toolchain GCC pour ARM :

add_custom_command(TARGET program.out
                   POST_BUILD
                   COMMAND arm-none-eabi-size program.out)

Créer un fichier fichier hex à partir de l’exécutable elf et supprimer ce hex quand ‘clean’ est invoquée

Cette astuce (très spécifique pour des projets embarqués, je l’avoue) est aussi basé sur add_custom_command() :

add_custom_command(TARGET program.out
                   POST_BUILD
                   COMMAND arm-none-eabi-objcopy.exe -I elf32-little -O ihex
                           program.out program.hex)

Si besoin, j’ai écrit il y a longtemps un article sur (arm-none-eabi-)objcopy et les paramètres à utiliser pour réaliser une telle conversion.

Ce fichier n’est bien sûr pas supprimé quand vous faites appel à la cible clean puisque CMake n’est pas vraiment au courant de ce fichier program.hex… Je ne n’ai pas trouvé de solution portable pour le supprimer mais si vous utilisez make derrière CMake, vous pouvez utiliser la variable ADDITIONAL_MAKE_CLEAN_FILES :

set_directory_properties(PROPERTIES ADDITIONAL_MAKE_CLEAN_FILES ${PROJECT_NAME}.hex)

Exclure des éléments du build quand on utilise GLOB_RECURSE

Utiliser file(GLOB_RECURSE …) est souvent considéré comme une mauvaise pratique en CMake. Mais bon, des fois c’est pratique… Si voulez exclure certains fichiers ou dossiers, il faut ensuite modifier la liste obtenue. Voici un exemple :

# Lister les fichiers dans 'source/'
file(GLOB_RECURSE SOURCE_FILES source/*.cpp source/*.hpp source/*.c source/*.h)

# Enlever le dossier 'source/stm32f4xx'
list(FILTER SOURCE_FILES EXCLUDE REGEX ${CMAKE_SOURCE_DIR}/source/stm32f4xx/*)

# Enlever le fichier 'source/debug/Hardfault.c'
list(REMOVE_ITEM SOURCE_FILES ${CMAKE_SOURCE_DIR}/source/debug/Hardfault.c)

pthread

Si votre projet sous Linux utilise des pthreads et si vous vous mangez des undefined references vers des fonctions de pthread lors de l’édition des liens, alors commencez par vérifier que la bibliothèque est bien installée sur votre système puis regardez du côté du module FindThreads. Voici un bout de code à rajouter à votre CMakeLists.txt :

find_package (Threads)
add_executable (myapp main.cpp ...)
target_link_libraries (myapp ${CMAKE_THREAD_LIBS_INIT})

 

PSP et MSP sur ARM Cortex-M

Si vous jetez un oeil aux registres d’un processeur ARM Cortex-M, vous ne serez pas surpris de voir Stack Pointer (SP) mais vous serez peut-être plus dubitatifs en voyant qu’il y a aussi Main Stack Pointer (MSP) et Process Stack Pointer (PSP). Il y a en fait deux stack pointers (MSP et PSP) et le registre SP contient soit la valeur de l’un, soit la valeur de l’autre. Laquelle ? Il faut regarder le bit 1 (SPSEL) du registre Control : s’il est à 1, le processeur utilise le PSP ; sinon, il utilise le MSP.

Si vous vous demandez à quoi cela sert, la réponse est assez simple : c’est essentiellement fait pour améliorer la robustesse du système lors de l’utilisation d’un système d’exploitation. Chris Shore (de chez ARM) explique pourquoi dans cette discussion :

Having two separate stack pointers allows the operating system to be safer and more robust. Usually, you would configure the operating system to use Main Stack Pointer (MSP) and user applications to use Process Stack Pointer (PSP). The switch from one stack to another then happens automatically when an exception is handled.

The fact that the operating system and exception handlers use a different stack from the application means that the OS can protect its stack and prevent applications from accessing or corrupting it. You can also ensure that the OS does not run out of stack if the application consumes all the available PSP stack space – that means that there is always space on the stack to run an exception handler in the case of an error occurring.

Note that you don’t have to use both stack pointers. By default, the system will only use a single stack pointer (MSP) and must be manually configured to use PSP. Also, some Cortex-M microcontrollers do not support two stack pointers.

Il est très simple de constater ce comportement de changement de stack pointer. Dans une application utilisant un OS comme FreeRTOS, il suffit de mettre un breakpoint dans une task de l’OS et un autre dans un handler d’interruption. Quand les breakpoints sont touchés et votre application se met en pause, jettez un oeil aux registres et à la callstack. Dans le premier cas, le bit 1 de Control est à 1, on est bien en Process Stack Pointer : dans le second cas, ce bit est à 0, on est bien en Main Stack Pointer.

Voici ce que ça donne quand on est s’arrête dans une task. On constate que les registres SP et PSP sont égaux :

Le débogueur arrive même à nous dire que le PSP pointe vers une zone dans ucHeap. C’est cohérent avec le fait que j’ai créé dynamiquement la tâche FreeRTOS et donc que sa stack a été allouée dans le heap de FreeRTOS.

Voici ce que ça donne pour une interruption. On voit que la valeur des registres SP et MSP sont égales :

Le PSP nous permet de voir que la tâche qui a été interrompue était l’idle task. La callstack nous montre aussi que les interruptions se font dans le même contexte que l’OS. prvPortStartFirstStack() est la fonction par laquelle démarre FreeRTOS et donne la main aux tasks en modifiant le stack pointer.

Empêcher l’ouverture d’une pop-up quand un programme Windows plante

Par défaut, quand votre programme C ou C++ plante sous Windows, une pop-up s’ouvre pour vous l’annoncer. Ça donne quelque chose comme ceci (avec mingw-w64 dans Eclipse) :

C’est sympa, mais ça peut poser des problèmes. En particulier, cela implique que votre programme continue de s’exécuter jusqu’à ce que quelqu’un clique sur le bouton « Fermer le programme« . Et ce quelqu’un n’arrivera peut-être jamais… C’est notamment le cas quand votre application crashe sur votre serveur d’intégration continue. L’application ne fait rien mais ne s’arrête pas sans que l’outil d’intégration continue ne puisse faire quoique ce soit (en fait, il ne le sait même pas), à part finir par faire un abort de la construction quand le timeout aura été atteint (si vous en avez configurer un…).

La solution est très simple, il suffit de rajouter une petite ligne au début de votre main() (en ayant bien sûr inclus le bon fichier d’en-tête) :

#include  <windows.h>

int main() {
    SetErrorMode (SEM_FAILCRITICALERRORS |
          SEM_NOGPFAULTERRORBOX | SEM_NOOPENFILEERRORBOX);

    // Votre programme

Votre programme s’arrêtera et le code de retour vous montera que le programme a planté :

Ça fonctionne aussi quand une exception n’est pas catchée ou quand votre programme rencontre un assert() qui échoue !

Pour plus de détails sur la fonction SetErrorMode():

  • sa documentation sur MSDN (vous y lirez ce magnifique commentaire : « Best practice is that all applications call the process-wide SetErrorMode function with a parameter of SEM_FAILCRITICALERRORS at startup. This is to prevent error mode dialogs from hanging the application. » =)
  • son implémentation dans mingw64

FreeRTOS Thread Debugging with Eclipse and OpenOCD

Si vous utilisez FreeRTOS et que votre IDE est Eclipse et que vous vous connectez à votre carte grâce à OpenOCD, ceci devrait vous intéresser !

MCU on Eclipse

FreeRTOS is probably the number one RTOS used, and Eclipse is likely the most popular IDE I can think of. But debugging FreeRTOS applications with Eclipse and GDB is somewhat limited? What I would like to get at the minimum is this: ability to see all the different threads in the Eclipse debug view like this:

FreeRTOS Threads in Eclipse with OpenOCD FreeRTOS Threads in Eclipse with OpenOCD

As you might guess from that screenshot: this post is about how to make FreeRTOS tread debugging possible with Eclipse and GDB :-).

Voir l’article original 594 mots de plus

Formater du texte en C++ avec {fmt}

Formater du texte en C++, c’est pas ultra fun… Il y a bien les « bons » vieux reliquats du C avec les fonctions genre std::printf() et les méthodes un peu plus modernes comme std::ostringstream. C’est pas génial comparer à string.format() de Python En regardant le code de spdlog( une bibliothèque de logging qui semble très bien mais qui malheureusement n’était pas compatible avec mes contraintes), j’ai découvert la bibliothèque {fmt}. C’est elle qui sert de back-end à spdlog pour formater les messages. Si vous voulez essayer {fmt}, voici le mode opératoire !

Builder la bibliothèque

Tout le code est sur GitHub, est placé sous licence BSD et se builde avec CMake. Le mode opératoire est donc très classique :

$ git clone https://github.com/fmtlib/fmt.git
$ cd fmt
p$ mkdir build
$ cd build
$ cmake ..
[...]
$ make all
Scanning dependencies of target fmt
[  2%] Building CXX object CMakeFiles/fmt.dir/src/format.cc.o
[  4%] Building CXX object CMakeFiles/fmt.dir/src/posix.cc.o
[  7%] Linking CXX static library libfmt.a
[  7%] Built target fmt
Scanning dependencies of target gmock
[  9%] Building CXX object test/CMakeFiles/gmock.dir/gmock-gtest-all.cc.o
[ 12%] Linking CXX static library libgmock.a
[ 12%] Built target gmock
Scanning dependencies of target test-main
[ 14%] Building CXX object test/CMakeFiles/test-main.dir/test-main.cc.o
[ 17%] Building CXX object test/CMakeFiles/test-main.dir/gtest-extra.cc.o
[ 19%] Building CXX object test/CMakeFiles/test-main.dir/util.cc.o
[ 21%] Linking CXX static library libtest-main.a
[ 21%] Built target test-main
Scanning dependencies of target time-test
[ 24%] Building CXX object test/CMakeFiles/time-test.dir/time-test.cc.o
[ 26%] Linking CXX executable ../bin/time-test
[...]
[100%] Linking CXX executable ../bin/util-test
[100%] Built target util-test

$ make test
Running tests...
Test project /home/pgradot/Documents/GitHub/fmt/build
      Start  1: assert-test
 1/11 Test  #1: assert-test ......................   Passed    0.02 sec
      Start  2: gtest-extra-test
 2/11 Test  #2: gtest-extra-test .................   Passed    0.03 sec
      Start  3: format-test
[...]
      Start 10: posix-mock-test
10/11 Test #10: posix-mock-test ..................   Passed    0.20 sec
      Start 11: posix-test
11/11 Test #11: posix-test .......................   Passed    4.85 sec

100% tests passed, 0 tests failed out of 11

Total Test time (real) =   5.84 sec


$ sudo make install
[  7%] Built target fmt
[...]
[100%] Built target format-impl-test
Install the project...
-- Install configuration: "Release"
-- Installing: /usr/local/lib/cmake/fmt/fmt-config.cmake
-- Installing: /usr/local/lib/cmake/fmt/fmt-config-version.cmake
-- Installing: /usr/local/lib/cmake/fmt/fmt-targets.cmake
-- Installing: /usr/local/lib/cmake/fmt/fmt-targets-release.cmake
-- Installing: /usr/local/lib/libfmt.a
-- Installing: /usr/local/include/fmt/core.h
-- Installing: /usr/local/include/fmt/format.h
-- Installing: /usr/local/include/fmt/format-inl.h
-- Installing: /usr/local/include/fmt/locale.h
-- Installing: /usr/local/include/fmt/ostream.h
-- Installing: /usr/local/include/fmt/printf.h
-- Installing: /usr/local/include/fmt/time.h
-- Installing: /usr/local/include/fmt/posix.h

J’ai volontairement raccourci la sortie de plusieurs commandes (vous avez sans doute vu les […]). Vous n’êtes pas obligé de faire make test mais c’est sympa de vérifier que notre bibliothèque s’est correctement compilée. On voit que l’installation a copié dans des dossiers classiques la bibliothèque statique libfmt.a ainsi que les nombreux headers dont nous aurons besoin pour l’utiliser dans notre code. Vous pourriez ne pas l’installer et récupérer les fichiers pour les mettre dans votre projet ou encore utiliser directement le CMake de {fmt} comme un sous-CMake de votre projet.

Petits essais

J’ai repris quelques lignes données dans le README du GitHub et j’ai écrit une petite fonction log() puisque c’est un peu pour ça que je me suis intéressé à {fmt} :

#include 
#include 
#include 

namespace pgt
{
template 
void log(const char* format, Args&& ... args)
{
	try
	{
		auto now = std::time(nullptr);
		auto timestamp = std::string(std::ctime(&now));
		std::replace(timestamp.begin(), timestamp.end(), '\n', '\0');

		auto message = fmt::format(format, args...);
		fmt::print("[{}] {}\n", timestamp, message);
	}
	catch(const fmt::format_error& e)
	{
		fmt::print("Invalid log message: {}\n", e.what());
	}
}
}

int main()
{
	// Exemples de GitHub
	fmt::print("Hello, {}!\n", "world");
	fmt::printf("Hello, %s!\n", "world");
	std::string s = fmt::format("{0}{1}{0}\n", "abra", "cad");
	fmt::print(s);

	// Log
	int speed = 1200;
	pgt::log("Speed = {} rpm - Temperature = {}°C", speed, 42);
	pgt::log("Setpoint = {}");
}

On compile et on lance le programme :

$ g++ -Wall -Wextra -std=c++11 main.cpp -lfmt && ./a.out
Hello, world!
Hello, world!
abracadabra
[Fri May 11 11:59:13 2018] Speed = 1200 rpm - Temperature = 42°C
Invalid log message: argument index out of range

Notes :

  1. C’est pas une super bonne idée de faire une fonction qui s’appelle log dans le namespace global, elle risque d’entrer en conflit avec std::log, qui dans mon cas était disponible (le test a été fait avec un gcc 5.4). Ainsi, un appel à log(42) compilait sans erreur alors que pgt::log(42) génère bien un message d’erreur.
  2. Il y a moyen de faire encore mieux pour formater le timestamp des logs mais mon vieux compilateur semblait perdu…
  3. Si on intervertit main.cpp et -lfmt dans la ligne de commande, on se mange une palanquée d’erreurs de link.
  4. Oui, un formatage peut rater et lancer des exceptions !

Personnellement, je trouve ça bien sympa !

Pour aller plus loin, vous pouvez regarder la documentation de l’API ou approfondir sur la syntaxe du formatage,

C’est tout pour aujourd’hui 🙂