Hier, ce blog a eu 7 ans. Quand je l’ai créé, je ne m’étais pas vraiment posé la question de sa durée de vie… Je n’avais sans doute pas pensé que ça durerait aussi longtemps (7 ans donc), que je posterai autant d’articles (142) qui seraient vus par autant de visiteurs (170 812 pour un total de 226 395 vues). Pour fêter cet anniversaire, j’ai choisi un sujet qui a animé de nombreux articles depuis la création de ce blog : le C et ses écueils (et par extension le C++ est concerné). Aujourd’hui, on va parler d’un résultat inattendu d’une division et on va faire un tour de la norme pour tirer les choses au clair !
Question à 1 dollar : que retourne le code suivant ?
int main() {
int a = -6;
unsigned int b = 3;
int c = a / b;
return c;
}
Puisque je pose la question, vous vous doutez bien que ça ne retourne pas -2. Non, sur Windows 10 avec mingw64, ça retourne 1431655763.
Mais alors pourquoi ? Lors de la division, a est promu en unsigned int. Le calcul est donc fait entre deux entiers non signés puis remis dans un signé. Voilà, c’est aussi simple que ça.
Vous n’êtes pas contents ? Vous pensiez que les options -Wall -Wextra verraient ce genre de problèmes ? Je l’ai dit et je le répète encore : ces options sont le minimum vital mais il y en a bien d’autres à activer pour se protéger. Et surtout, c’est un comportement parfaitement normé du langage. Il suffit de se rendre à la section 6.3.1.8 Usual arithmetic conversions de la norme C99 (document n1256) pour y lire :
if the operand that has unsigned integer type has rank greater or
equal to the rank of the type of the other operand, then the operand with
signed integer type is converted to the type of the operand with unsigned
integer type
En fait, il y a moyen d’avoir un avertissement du compilateur. Il suffit de rajouter l’option -Wsign-conversion pour obtenir deux warnings sur la même ligne :
warning: conversion to 'unsigned int' from 'int' may change
the sign of the result [-Wsign-conversion]
warning: conversion to 'int' from 'unsigned int' may change
the sign of the result [-Wsign-conversion]
Le premier correspond à la conversion de b en unsigned int ; le second à la conversion du résultat en int pour l’affecter à c.
cppcheck dit aussi de faire attention :
cppcheck: (warning) Suspicious code: sign conversion of a in calculation,
even though a can have a negative value
Notez au passage que a + b, a -b et a * c produisent les bons résultats (au moins sur mon PC, avec ma version de compilateur). Les 4 opérations génèrent des warnings avec GCC mais seules la multiplication et la division génèrent un warning de cppcheck.
PS : merci à Pulkomandy pour m’avoir posé cette colle :p
Bon nombre de développeurs embarqués pensent que printf() et scanf() sont réservés au monde PC et qu’ils doivent déboguer à la LED et aux combinaisons de boutons-poussoirs. Il est pourtant souvent facile d’avoir une UART réservée au debug pour lire et écrire des bytes et ainsi utiliser printf() et scanf(). Plus que printf() et scanf(), il s’agit en fait d’avoir une sortie standard (stdout) et une entrée standard (stdin) et ainsi d’avoir accès aux fonctions de la bibliothèque standard. Si stdin n’est pas forcément utile tous les jours, stdout l’est vraiment pour mettre des logs et ainsi suivre l’exécution de son programme.
Comment faire ?
En voilà une bonne question ! Il n’y a pas de réponse type malheureusement… Ça dépend de votre toolchain et surtout de la libc utilisée. Il faut fouiller dans la documentation et / ou sur Internet pour trouver quoi faire. En général, il va s’agir de redéfinir deux fonctions, une pour la lecture et l’autre pour l’écriture.
La réponse est malheureusement la même que pour la question précédente. Ça dépend aussi bien sûr de ce que vous utilisez.
De simples puts() et fgets() ne seront pas très coûteux. Un printf("%d", value) coûtera un peu plus. Un printf("%f", value) coûtera encore plus. Et bien sûr, quand vous commencez à faire #include <iostream> et à jouer avec std::cout et std::cin, alors là, ça peut monter très vite… Je vous encourage à faire des essais, à voir ce qui coûte un peu, beaucoup, trop et trouver des compromis entre réutiliser des fonctions standards et implémenter certains fonctions simplifiées par vous même. Certains linkers sont plus intelligents que d’autres et ont des flags particuliers pour embarqué ou pas le code correspondant à un formateur particulier. Par exemple, ld (le linker de GCC) à les options -u _printf_float et -u _scanff_float pour pouvoir utiliser %f.
Avec la toolchain GNU pour ARM que j’utilise sur STM32, les printf() coûtent peu, en flash comme en RAM, y compris avec le formateur %f. En revanche, le simple fait d’inclure iostream (sans rien toucher à ce qu’il y a dedans) coûte 140k de flash et 6k de RAM. J’ai donc créé mes propres fonctions avec plusieurs surcharges pour avoir avec une syntaxe type stream << value << otherValue et j’utilise printf() de la bibliothèque standard. J’ai trouvé un bon compromis entre temps de développement, occupation mémoire et fonctionnalités.
Des fois, vous n’avez aucune contrainte. Dans certains applications particulières, par exemple des logiciels de tests ou de configuration qui ne sortent pas de l’usine, j’utilise à fond std::cout et std::cin. J’utilise la bibliothèque standard C++ de manière décomplexée, avec notamment std::getline et std::map. Oui, j’ai un terminal sur mon système embarqué, ça marche nickel et ça tient en quelques lignes de code !
Enfin, soyez conscients que toutes les fonctionnalités des bibliothèques standard C comme C++ peuvent utiliser de l’allocation dynamique (sauf quelques unes qui le précisent explicitement comme std::array). J’ai par exemple constaté avec ma toolchain qu’inclure iostream allouait de la mémoire (avant main() donc) et que le première appel (mais pas les suivants) à printf() faisait aussi une allocation.
Un exemple d’implémentation ?
Oui, avec la toolchain GNU pour ARM, il faut implémenter deux fonctions que le linker prendra magiquement. Attention, elles doivent avoir un linkage C !
#include "drivers/Uart.hpp"
static drivers::Uart uart(USART3);
extern "C" {
int _write(int /*fd*/, const void *buf, size_t count) {
uart.sendSeveral(buf, count);
return count;
}
int _read(int /*fd*/, const void* buf, size_t count) {
std::size_t received = 0;
auto line = (std::uint8_t*) buf;
while (received &amp;lt; count) {
auto c = uart.read();
line[received] = c;
++received;
if (c == '\n') {
// Stop when '\n' is received
break;
}
}
return received;
}
Sur Cortex-M, il est possible d’utiliser le registre DWT CYCCNT pour avoir une mesure d’être précise du temps. Enfin, t’as tout à fait du temps : du nombre de cycles CPU écoulés. Un savant calcul avec la fréquence processeur redonne un temps… C’est utile pour mesurer les performances d’un morceau de code, en ayant conscience que le nombre de cycles nécessaires pour l’exécution doit tenir dans ce registre de 32 bits (ça laisse de la marge).
Voici comment s’en servir :
volatile int a;
volatile int b;
volatile int c;
void run() {
std::cout << "Application starts" << std::endl;
// Check it DWT is present
if ((DWT->CTRL | DWT_CTRL_NOCYCCNT_Msk) == 1) {
std::cout << "ERROR: DWT CYCCNT is not supported" << std::endl;
} else {
// Enable
DWT->CTRL |= 1;
// Restart counter
DWT->CYCCNT = 0;
// Do something
c = (a + b) * (a - b) * 2;
// Get cycles
auto cycles = DWT->CYCCNT;
std::cout << "Execution took " << (int) cycles << " cycles" << std::endl;
}
while (1) {
}
}
En exécutant ce code sur une carte Nucleo de ST, équipée d’un STM32F413 (Cortex-M4), j’obtiens :
Application starts
Execution took 15 cycles
Si je remplace le calcul par un __NOP(), alors le nombre de cycle est de 1 et si je ne met rien, il est de… 0. Si je fais un délai de 1 seconde, avec HAL_Delay(1000), le nombre de cycles est de 32.028.150, ce qui est cohérent avec le fait que mon CPU tourne à 32 MHz.
En écrivant des wrappers C++ pour FreeRTOS, j’ai eu besoin de savoir si le code était appelé depuis une interruption ou pas. En effet, plusieurs fonctions de FreeRTOS possède une variante avec le suffixe fromISR : il faut appeler la version normale ou la version fromISR aux bons moments. Quand on utilise un Cortex-M, il faut regarder du côté du System Control Block (SCB) et plus particulièrement l’Interrupt Control and State Register (ICSR). Voici une fonction tout simple, prise sur stackoverflow, et qui fait le taf :
Voici un exemple d’utilisation pour mon wrapper de sémaphore :
bool AbstractSemaphore::give() {
BaseType_t result = isInInterrupt() ?
xSemaphoreGiveFromISR(nativeHandle_m, nullptr) :
xSemaphoreGive(nativeHandle_m);
return result == pdTRUE;
}
Il m’est ainsi possible d’appeler la fonction give() sans me soucier de savoir si l’appel se fait depuis une IT ou pas. Attention, cela ne veut pas dire qu’il ne faut se soucier de rien ! En effet, il existe dans FreeRTOS un mécanisme empêchant l’appel des fonctions dites syscalls depuis une IT dont la priorité est supérieure à un seuil. Ce seuil est configuré dans FreeRTOSConfig.h avec la constante configMAX_SYSCALL_INTERRUPT_PRIORITY (pour plus de détails, voir la documentation de FreeRTOS) :
/* The highest interrupt priority that can be used by any interrupt service
routine that makes calls to interrupt safe FreeRTOS API functions. DO NOT CALL
INTERRUPT SAFE FREERTOS API FUNCTIONS FROM ANY INTERRUPT THAT HAS A HIGHER
PRIORITY THAN THIS! (higher priorities are lower numeric values. */
#define configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY 5
/* Interrupt priorities used by the kernel port layer itself. These are generic
to all Cortex-M ports, and do not rely on any particular library functions. */
#define configKERNEL_INTERRUPT_PRIORITY ( configLIBRARY_LOWEST_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
/* !!!! configMAX_SYSCALL_INTERRUPT_PRIORITY must not be set to zero !!!!
See http://www.FreeRTOS.org/RTOS-Cortex-M3-M4.html. */
#define configMAX_SYSCALL_INTERRUPT_PRIORITY ( configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY << (8 - configPRIO_BITS) )
En fait, la valeur vraiment utile est celle de configLIBRARY_MAX_SYSCALL_INTERRUPT_PRIORITY : 5. Pour rappel, plus la valeur est grande, plus la priorité est faible. Dans ma configuration actuelle, cela signifie que je peux appeler give() depuis mon IT d’external interrupt seulement si la priorité d’interruption est configurée pour x => 5 :
Il est possible de rajouter sa propre assertion dans le wrapper C si on a une meilleure remontée des erreurs qu’un simple blocage de l’application. Il est aussi possible de redéfinir la macro configASSERT() dans FreeRTOSConfig.h (voir à nouveau la documentation), qui par défaut est définie ainsi :
#define configASSERT( x ) if ((x) == 0) {taskDISABLE_INTERRUPTS(); for( ;; );}
Il y a plusieurs années, je m’étais intéressé à la manière d’appeler du code C depuis Python. C’était tellement compliqué que je n’ai jamais eu envie d’essayer. J’ai récemment découvert l’existence de Boost Python, et là, j’ai eu envie d’essayer ! C’est parti !
Créer son module Python
La première étape est bien sûr d’écrire quelques fonctions ou classes C++ et de faire un peu de magie pour faire les wrappers pour Python. Pour créer un module Python, il suffit de créer une bibliothèque dynamique avec ce code et CMake est bien sûr l’outil de choix pour cela. Voici un exemple :
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters. Learn more about bidirectional Unicode characters
$ mkdir build
$ cd build/
$ cmake ..
-- The C compiler identification is GNU 7.3.0
-- The CXX compiler identification is GNU 7.3.0
-- Check for working C compiler: /usr/bin/cc
-- Check for working C compiler: /usr/bin/cc -- works
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Detecting C compile features
-- Detecting C compile features - done
-- Check for working CXX compiler: /usr/bin/c++
-- Check for working CXX compiler: /usr/bin/c++ -- works
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Detecting CXX compile features
-- Detecting CXX compile features - done
CMake Warning at /usr/share/cmake-3.10/Modules/FindBoost.cmake:1626 (message):
No header defined for python3; skipping header check
Call Stack (most recent call first):
CMakeLists.txt:5 (find_package)
-- Boost version: 1.65.1
-- Found the following Boost libraries:
-- python3
-- Found PythonInterp: /usr/bin/python3 (found suitable version "3.6.7", minimum required is "3")
-- Found PythonLibs: /usr/lib/i386-linux-gnu/libpython3.6m.so (found suitable version "3.6.7", minimum required is "3")
-- Configuring done
-- Generating done
-- Build files have been written to: /home/pierre/Documents/boost_python/build
$ cmake --build .
Scanning dependencies of target mylibrary
[ 50%] Building CXX object CMakeFiles/mylibrary.dir/functions.cpp.o
[100%] Linking CXX shared library mylibrary.so
[100%] Built target mylibrary
On peut maintenant exécuter Python et tester notre module :
$ python3
Python 3.6.7 (default, Oct 22 2018, 11:32:17)
[GCC 8.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import mylibrary
>>> mylibrary.say_hello()
Hello
>>> p = mylibrary.Printer("B&W printer")
>>> p.print("bla bla")
[B&W printer]bla bla
>>>help(mylibrary)
Help on module mylibrary:
NAME
mylibrary
CLASSES
Boost.Python.instance(builtins.object)
Printer
class Printer(Boost.Python.instance)
| Method resolution order:
| Printer
| Boost.Python.instance
| builtins.object
|
| Methods defined here:
|
| __init__(...)
| __init__( (object)arg1, (str)arg2) -> None :
|
| C++ signature :
| void __init__(_object*,std::__cxx11::basic_string<char, std::char_traits, std::allocator >)
|
| __reduce__ = (...)
|
| print(...)
| print( (Printer)arg1, (str)arg2) -> None :
|
| C++ signature :
| void print(Printer {lvalue},std::__cxx11::basic_string<char, std::char_traits, std::allocator >)
|
| ----------------------------------------------------------------------
| Data and other attributes defined here:
|
| __instance_size__ = 32
|
| ----------------------------------------------------------------------
| Methods inherited from Boost.Python.instance:
|
| __new__(*args, **kwargs) from Boost.Python.class
| Create and return a new object. See help(type) for accurate signature.
|
| ----------------------------------------------------------------------
| Data descriptors inherited from Boost.Python.instance:
|
| __dict__
|
| __weakref__
FUNCTIONS
compute(...)
compute( (int)arg1, (int)arg2) -> int :
C++ signature :
int compute(int,int)
sayHelloTo(...)
sayHelloTo( (str)arg1) -> None :
C++ signature :
void sayHelloTo(std::__cxx11::basic_string<char, std::char_traits, std::allocator >)
say_hello(...)
say_hello() -> None :
C++ signature :
void say_hello()
FILE
/home/pierre/Documents/boost_python/build/mylibrary.so
Notez que import mylibrary fonction.ne parce que mylibrary.so est dans le dossier d’où Python est lancé. Il y a moyen de l’installer dans le dossier dédié de Python, comme expliqué ici, pour y avoir accès depuis n’importe où.
Évidemment, tout n’a pas marché du premier coup… Je me suis tapé quelques erreurs sympas avant d’arriver à quelque chose de fonctionnel.
Il faut bien sûr que Boost et Python soient installés :
sudo apt install libboost-all-dev python3-dev
J’ai eu une erreur magnifique de compilation à cause d’un header Python :
$ make
[ 50%] Building CXX object CMakeFiles/mylibrary.dir/functions.cpp.o
In file included from /usr/include/boost/python/detail/prefix.hpp:13:0,
from /usr/include/boost/python/args.hpp:8,
from /usr/include/boost/python.hpp:11,
from /home/pierre/Documents/boost_python/functions.cpp:1:
/usr/include/boost/python/detail/wrap_python.hpp:50:11: fatal error: pyconfig.h: No such file or directory
# include
^~~~~~~~~~~~
compilation terminated.
Ce fichier est apporté par le paquet python3-dev et il était bien présent sur mon PC :
Le problème était donc d’ajouter le dossier /usr/include/python3.6m/ à l’include path. Une solution est de l’ajouter au path avant de compiler. Il y a beaucoup mieux en s’appuyant que les capacités de CMake à trouver Python : si Python est trouvé, la variable PYTHON_INCLUDE_DIRS est renseignée et il suffit de l’ajouter en tant qu’include directory comme fait dans mon CMakeLists.txt ci-dessus.
La première fois que j’ai tenté d’importer mon module, j’ai eu une erreur de version de Python :
$ python3
Python 3.6.7 (default, Oct 22 2018, 11:32:17)
[GCC 8.2.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> import mylibrary
Traceback (most recent call last):
File "", line 1, in
ImportError: /usr/lib/i386-linux-gnu/libboost_python-py27.so.1.65.1: undefined symbol: PyClass_Type
>>>
[3]+ Stopped python3
En effet, find_package(Boost COMPONENTS python) trouvait la variante pour Python 2. Il m’a fallu rajouter un 3, ce qui donne find_package(Boost COMPONENTS python3). La documentation de FindBoost a un paragraphe à ce sujet :
Note that Boost Python components require a Python version suffix (Boost 1.67 and later), e.g. python36 or python27 for the versions built against Python 3.6 and 2.7, respectively. This also applies to additional components using Python including mpi_python and numpy. Earlier Boost releases may use distribution-specific suffixes such as 2, 3 or 2.7. These may also be used as suffixes, but note that they are not portable.