Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

AVERTISSEMENT

Ce livre mdbook est à l’étape du projet de création et dans son format brouillon (draft) pour le moment.

Il peut comporter des défauts ou des erreurs et sa forme comme son contenu évoluent continuellement.

Merci de votre compréhension.

Si vous souhaitez m’apporter votre soutien ou votre concours pour ce projet, vous pouvez me contacter.

Marc JESTIN
éco-citoyen du monde
https://marcjestin.fr

Introduction

Ce livre est un travail personnel.

J’ai choisi de le publier et de le partager pour « faire ma part ».

La consultation de ce livre est « en don libre ».

Oui, vous pouvez

Bien que l’argent et les biens matériels n’aient jamais été, ne soient pas, et ne seront jamais ma valeur première, j’en ai besoin pour vivre décemment.

Si vous souhaitez m’encourager ou m’aider, vous pouvez.

  • effectuer un don.
    • Il vous suffit de me contacter, je vous communiquerai mes coordonnées bancaires.
  • me proposer un travail rémunéré salarié
    • sédentaire
    • en télétravail
    • ou dans un bureau
      • si ce n’est pas dans une grande agglomération
      • si c’est au Nord de la France (Bretagne, Normandie, Nord)
        • ou plus au Nord (l’Irlande me plairait tout particulièrement)
    • qui que vous soyez :
      • entreprise ;
      • administration ;
      • association ou
      • particulier·e.

Bonne lecture

Si vous rencontriez des erreurs, si vous souhaitiez suggérer des ajouts, ou pour toute autre démarche constructive et pertinente, n’hésitez pas à me contacter.

Merci.

Marc JESTIN,
éco-citoyen du monde
https://marcjestin.fr

Installer Rust (default)

Pour travailler avec Rust, nous avons besoin de mettre en place ses outils.

La procédure d’installation est très simple.

Installation dans Linux

Pour installer Rust et ses outils dans un environnement sain, il suffit de suivre la procédure indiquée ici : https://rust-lang.org/tools/install/.

La commande à exécuter :

curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Si curl n’est pas déjà installé dans notre système, il suffit d’installer le paquet de même nom (dans toutes les distributions).

  • Debian et dérivées : # apt install curl
  • Fedora : # dnf install curl
  • Arch Linux : # pacman -S curl
  • openSUSE : # zypper install curl

Chargement

Après installation, nous pouvons

  1. exécuter : source "$HOME/.cargo/env" ou
  2. démarrer un nouveau Terminal (qui se chargera de le faire via le .bashrc ou équivalent).

Ceci fait, le shell devrait en principe nous donner accès aux commandes rustc, cargo, rustup

La plupart des guides proposent de saisir rustc --version mais une commande sans argument permet tout aussi bien de savoir si tout va bien… Si l’aide s’affiche, c’est que le shell l’a bien trouvée.

Désinstallation

Pour désinstaller Rust et ses outils, nous disposons de la commande

rustup self uninstall

Écrire du code Rust

Écrire un premier code

Une fois Rust installé dans notre système, nous pouvons nous servir de ses outils.

Nous pouvons écrire un code source Rust n’importe où dans notre système de fichiers.

Par convention, les fichiers sources Rust portent une fin de nom (non, on ne dit pas extension) .rs, par exemple mon_tout_premier_code.rs.

Nous pouvons écrire ce petit code source de programme :

fn main () {
    println!("Rust est formidable !");
}

Ceci fait, nous pouvons saisir cette commande (dans le même dossier) :

rustc mon_tout_premier_code.rs

Ceci fait, nous pouvons exécuter notre programme (toujours dans le même dossier) :

./mon_tout_premier_code

En pratique, nous utilisons rarement le compilateur rustc isolément. Nous nous servons plutôt de Cargo, le super-tanker de Rust qui dispose de nombreux super-pouvoirs bien pratiques.

Environnement de travail

Au delà de son compilateur rustc (excellentissime, comme nous le verrons par la suite), Rust propose de nombreux outils très aboutis que nous allons apprendre à connaître.

Un dicton dit

« On reconnaît un bon artisan au fait qu’il choisit bien ses outils. »

Je ne sais pas s’il est exact, mais je sais que l’environnement de travail Rust nous apporte indéniablement une palette d’outils de très bonne qualité.

Les principaux outils Rust

Pour travailler correctement et proprement avec Rust, nous avons besoin de connaître quelques commandes simples pour utiliser rustup, cargo, rustfmt, clippy et rustdoc.

rustup

rustup est l’installateur officiel et le gestionnaire de versions de Rust.

Il permet notamment :

  • d’installer et de mettre à jour les outils Rust.
  • de gérer plusieurs versions et de basculer de l’une à l’autre.
  • d’ajouter des cibles de compilation croisée (cross-compilation) pour créer des exécutables pour d’autres systèmes (Android, Windows, WebAssembly, etc.).
  • de télécharger les composants additionnels.

cargo

cargo permet de faire à peu près tout le reste, si nous nous y prenons bien.

Les commandes clefs à connaître sont :

  • cargo new pour créer un nouveau projet.
  • cargo run pour exécuter le projet en cours.
  • cargo test pour exécuter les tests du projet en cours.
  • cargo build pour compiler le projet en cours (si les deux précédentes qui se chargent de le faire ne suffisent pas).
  • cargo fmt (rustfmt).
  • cargo clippy.

rustfmt

rustfmt est un formateur de code. Il se charge de nous aider à présenter proprement le code.

rustfmt applique les règles de style officielles de la communauté Rust (indentation, espaces, sauts de lignes, placement des accolades). Cela garantit que tous les projets Rust partagent la même structure visuelle.

clippy

clippy est le linter (analyseur statique) de Rust.

Le compilateur rustc est déjà un assistant très qualitatif. clippy va plus loin en traquant les anti-patterns, les problèmes de performance potentiels, le code non idiomatique ou les erreurs de logique courantes.

rustdoc

rustdoc est le générateur de documentation Rust.

C’est un outil qui redonne envie de documenter, tellement il est bien conçu, à commencer par le support du langage Markdown.

Pour vous convaincre de sa qualité, je vous invite à faire une petite balade dans https://docs.rs. Il s’agit du site regroupant toutes les documentations des projets déposés sur les serveurs publics de la communauté.

TOUS les projets présents dans le dépôt des crates (paquet Rust) https://crates.io/ voient leur documentation publiée dans https://docs.rs.

Exemple :

Autre gros atout de la documentation rustdoc : elle relie la documentation avec le code source (dupliqué dans la documentation pour plus de commodité d’affichage).

Exemple :

Les lignes correspondant au code concerné (la fonction publique spawn ici) sont bien surlignées dans l’affichage et indiquées dans l’URL.

Créer un premier projet

Plutôt que de travailler dans des fichiers épars, Rust nous propose de structurer des projets (crates).

Organisation des codes sources

Pour illustrer ce propos, créons notre premier projet.

Par exemple dans ~/mesprojets, je saisis la commande

cargo new mon_premier_projet

Retour commande :

    Creating binary (application) `mon_premier_projet` package
note: see more `Cargo.toml` keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

Cargo s’est chargé de créer une structure de fichiers minimale qui se présente sous cette forme :

mon_premier_projet/
├── .git/
├── .gitignore
├── Cargo.toml
└── src/
    └── main.rs

Les deux éléments clefs que nous apprendrons à modifier bientôt sont :

  • Cargo.toml : fichier de configuration du projet ;
  • src/ : dossier de travail où nous disposons les codes sources de notre projet (pour le moment, un simple fichier main.rs).

(Compilation et) exécution du projet

Nous nous plaçons dans le dossier du projet pour travailler

cd mon_premier_projet

puis nous exécutons le projet avec

cargo run

Cargo se charge alors de compiler le projet puis de l’exécuter.

Retour commande :

   Compiling mon_premier_projet v0.1.0 (/home/user/mesprojets/mon_premier_projet)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.13s
     Running `target/debug/mon_premier_projet`

Affichage de notre programme :

Hello, world!

Il n’est pas possible de donner certains noms à nos projets du fait de l’organisation de Cargo. Par exemple, la commande cargo new test échoue avec un message explicatif très clair.

Organisation des fichiers et produits de compilation (build)

Si nous observons à nouveau l’arborescence de notre projet, elle se présente ainsi :

.
├── Cargo.lock
├── Cargo.toml
├── src
│   └── main.rs
└── target
    ├── CACHEDIR.TAG
    └── debug
        ├── build
        ├── deps
        │   ├── mon_premier_projet-0df163ce36d1f442
        │   └── mon_premier_projet-0df163ce36d1f442.d
        ├── examples
        ├── incremental
        │   └── mon_premier_projet-3quxu937gaaqi
        │       ├── s-hk8vkx5abq-15mx0e1-39psxwbzqm6jzctvvnaremlic
        │       │   ├── 6ebmlgxseqwl0atvo8e8mx1rq.o
        │       │   ├── 8orpsbst6x8f7sbtomzkztbr1.o
        │       │   ├── crwrq2j5zh4baxwcfl0gv3ic5.o
        │       │   ├── dep-graph.bin
        │       │   ├── eguk8f1vvf17rgcamp001y1cw.o
        │       │   ├── ewm5sh4zvjz3w4hyzio35bf8y.o
        │       │   ├── ey68lrkqdl5whqctqqc5tomw5.o
        │       │   ├── query-cache.bin
        │       │   └── work-products.bin
        │       └── s-hk8vkx5abq-15mx0e1.lock
        ├── mon_premier_projet
        └── mon_premier_projet.d

10 directories, 18 files

Rust a généré ses fichiers de travail dans le nouveau dossier target/ à la racine du projet.

Le fichier binaire compilé exécutable est target/debug/mon_premier_projet. Nous pouvons le copier ou le déplacer et l’exécuter à loisir (tout comme le fichier compilé que nous avions créé avec notre premier programme compilé avec rustc).

Mode debug et mode release

Comme son nom l’indique clairement, le dossier target/debug/ contient des fichiers destinés au débogage / développement du programme.

Le fichier binaire compilé exécutable target/debug/mon_premier_projet, quoique parfaitement utilisable, n’est pas le fichier qu’il est recommandé d’utiliser.

Création du fichier release

Lorsque nous y sommes prêts, nous pouvons passer à la construction d’une version release de notre programme.

Pour ce faire, nous pouvons utiliser :

cargo build --release # compilation (build) uniquement
# ET / OU
cargo run --release # compilation et exécution (run)

Le fichier ainsi généré, target/release/mon_premier_projet, est le fichier binaire de notre programme final.

Affichage des modes de compilation

Le retour commande est différent :

Version par défaut (debug)

Retour commande :

    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.09s

Version release

Retour commande :

    Finished `release` profile [optimized] target(s) in 0.06s

Utiliser examples/

Il peut être pratique de savoir écrire et exécuter des petits bouts de programmes Rust indépendants (ne serait-ce que pour réaliser les exercices que je propose dans ce livre).

Il existe une petite astuce dans Rust bien pratique, intégrée dans le fonctionnement de Cargo : les programmes exemples.

Ces programmes ont pour vocation, en principe, de démontrer des usages de bibliothèques écrites en Rust et dont nous allons créer notre toute première dès la prochaine page.

Nous pouvons nous servir de cette fonctionnalité native pour écrire des bouts de programmes indépendants dans un projet commun Rust. Nous pouvons alors les compiler et exécuter avec cargo run.

Fonctionnement des exemples

Il suffit de créer un dossier examples/ à la racine du projet et d’y écrire un code source Rust de programme mon_premier_exemple.rs. Ce programme doit comporter une fonction fn main().

Ceci fait, la commande

cargo run --example mon_premier_exemple # (sans le .rs)

compile (si nécessaire) et exécute le programme ainsi créé.

Mise en application

Nous écrivons le code source examples/mon_premier_exemple.rs suivant :

fn main() {
    println!("Ceci est mon premier exemple.");
}

puis nous l’exécutons.

Astuce : pour saisir le nom du code à compiler et exécuter, nous pouvons nous servir de l’autocomplétion du shell (touche tabulation).

Optimisation de code

Rust propose deux options de compilation :

  • debug : option utilisée par défaut, plus rapide ;
  • release : option recommandée pour la production des programmes, plus lente car le compilateur modifie certains comportements et réalise des optimisations de code.

Nous pouvons déjà utiliser l’option --release de cargo avec nos programmes exemples. Ce sera bien pratique pour tester différentes stratégies algorithmiques et leurs performances : nous verrons alors que les versions debug et les versions release ne se comportent pas de la même manière.

cargo run --release --example mon_premier_exemple # (sans le .rs)
Remarque

Les exemples dont nous parlons ici sont indépendants des exemples que nous insérons dans les documentations de nos librairies et programmes.

Documenter et tester notre code Rust

Je crois utile et important d’apprendre les bonnes pratiques dès les tout débuts de l’apprentissage d’un langage (ou de quoique ce soit d’autre).

Aussi, je vous propose d’apprendre tout de suite à écrire une documentation claire, à y fournir des exemples d’utilisation et à écrire des tests unitaires complémentaires qui n’apparaissent pas dans la documentation mais qui consolident durablement nos codes sources.

Nous allons utiliser un « fil rouge » très simple pour illustrer le propos et le mettre en pratique : nous allons construire une petite ébauche de librairie de fonctions mathématiques élémentaires.

J’ai choisi ce fil rouge pour que les codes sources que nous produisons soient à la fois les plus simples possibles et formateurs par eux-mêmes.

Écriture de notre fonction

Nous allons écrire le code d’une fonction qui calcule le périmètre d’un carré.

Nous devons d’abord apprendre comment s’organise un projet (crate) Rust : il se compose d’une librairie (ou API) et / ou d’un programme exécutable.

Librairie, programme

Dans un projet (crate) Rust, nous disposons généralement d’une partie API (librairie) et d’une partie programme.

Par convention, Cargo s’attend à trouver :

  • le code source d’un programme dans src/main.rs et / ou
  • le code source d’une librairie dans src/lib.rs.

Ceci est le comportement simple par défaut. Nous verrons par la suite comment donner des indications au compilateur pour utiliser d’autres fichiers et / ou comment ajouter des composants internes et externes au projet.

Mise en application

Nous créons un nouveau projet mon_api_maths en dehors de tout autre projet (dans ~/mesprojets dans mon cas).

Nous y créons le fichier src/lib.rs et écrivons notre code source, à commencer par la fonction souhaitée.

Code source sans documentation

Note au sujet des notations

Je m’assure ici et pendant quelques temps d’utiliser des notations de typage complètes et strictes.
Cela garantit une parfaite lisibilité EXPLICITE des codes afin que vous distinguiez mieux les types utilisés. 1
Ceci vous permet également d’apprendre ces conventions d’écriture.

Rassurez-vous, Rust pratique couramment l’inférence de type et nous pourrons nous passer de cette rigueur par la suite.
Ce d’autant plus que, comme nous le verrons, le compilateur Rust nous prémunit de toute erreur de typage à la compilation : si le code compile, c’est que nos types correspondent bien ou ont bien été inférés par le compilateur. Nous n’aurons pas de mauvaise surprise sur ce plan lors de l’exécution de nos programmes.

Les notations cote_u64: u64 = et 4_u64 ci-après ne sont absolument pas indispensables grâce à l’inférence de type. Il est toutefois utile de les connaître et de prendre l’habitude de « mentaliser » le type d’une valeur littérale lorsque nous l’écrivons ou la lisons dans nos codes Rust.

Code de notre fonction

#![allow(unused)]
fn main() {
pub fn calculer_perimetre(cote: u32) -> u64 {
    cote_u64: u64 = cote as u64;
    cote_u64 * 4_u64
}
}

Apprentissages

Cette fonction nous permet d’apprendre quelques notions de base essentielles :

  • Nous déclarons une fonction avec le mot-clef fn suivi du nom de la fonction calculer_perimetre ;
    • les conventions de nommage Rust recommandent le snake_case (mots séparés par un _) pour les noms de variables et de fonctions ;
  • (cote: u32) est la partie paramètres d’entrée de la fonction. Notre fonction a besoin d’un paramètre cote, de type u32.
    • u32 est le type entier non signé (positif ou nul) de 32 bits.
  • -> u64 est le type du retour de notre fonction.
    • u64 est le type entier non signé (positif ou nul) de 64 bits.
      • J’utilise cette astuce pour éviter un éventuel dépassement du maximum possible en u32. 1
  • {} les accolades délimitent ici le corps de notre fonction. Plus généralement, nous pouvons définir des blocs par des accolades à tout endroit dans nos codes Rust.
  • cote_u64 * 4 est une expression qui calcule ET constitue le retour de notre fonction.
    • Dans une fonction Rust, la dernière expression non suivie d’un ; est, par convention, le retour de cette fonction.
  • pub indique que nous souhaitons rendre la fonction disponible et accessible à tout programme qui le souhaite.

Nous le verrons, Rust n’autorise pas les opérations sur des variables de types différents.
C’est la raison pour laquelle, si je souhaite renvoyer un résultat de type u64 , je dois d’abord convertir l’argument passé à ma fonction dans une variable du même type.
Je le fais avant le calcul. Si je le faisais sur une valeur calculée qui serait (cote * 4) en u32, je perdrais tout l’intérêt de cette astuce préventive. 1

Notes d'approfondissement

Signature de notre fonction

Pour des raisons que nous aborderons plus tard, Rust distingue certains types simples dont les entiers font partie. Nous passons donc bien la valeur en argument et non une référence, sans pour autant pénaliser le code appelant notre fonction.

Type par défaut

Le compilateur Rust utilise l’inférence de type et associe des types par défaut aux variables en fonction de ce qu’il connaît et observe de nos codes sources.

De fait, le compilateur Rust — qui ne dispose pas « d’intelligence métier » — ne peut pas se douter que la longueur de côté d’un carré est nécessairement un entier positif ou nul (unsigned), idem pour le périmètre. Il ne sait même pas ce qu’est un « côté » ni un « carré » !

Le type « entier » par défaut de Rust est i32. C’est le type qui serait utilisé si nous ne précisions pas explicitement u32 pour le paramètre ni u64 pour le résultat ni… (le compilateur Rust analyse le code de manière plus profonde encore…).

Nous, nous savons que ce serait un non-sens d’utiliser i32.
Ce serait

  1. gaspiller la moitié des capacités d’encodage sur 32 bits ;
  2. risquer de créer une erreur conceptuelle grave dans le code en acceptant ou en générant des valeurs négatives sans que ce comportement ne soit interdit par notre code.

Il est donc important et intelligent de choisir un entier non signé dans ce cas.

Nous nous servons du typage statique fort de Rust pour nous « protéger ».

Je croise souvent des codes qui utilisent des types entiers signés alors que la « logique métier » impose des valeurs nécessairement positives ou nulles. Parfois par omission, parfois explicitement.
Dans le premier cas (laisser le compilateur choisir), c’est une faute grave.
Dans le second (demander explicitement un entier signé dans un contexte métier comme celui-ci), c’est une faute lourde.
On peut clairement se demander si nous ferions confiance à un·e développeur·se qui commet ce genre d’impair pour un code qui nous concerne personnellement (Spoiler : moi, non !)

Astuce de protection

L’astuce de protection que j’utilise en retournant un résultat u64 est un pis-aller.

Je m’en sers pour vous sensibiliser à l’importance de réfléchir à ce qui peut se produire avec nos codes.
Ici, nous pouvons stocker une valeur maximale u32 de 4_294_967_295. Si nous appelons la fonction avec une valeur de cote supérieure à 1_073_741_823, nous dépasserons la limite dans le résultat du calcul du périmètre.

J’ai choisi, dans cette première partie d’apprentissage, de rester au plus simple. Dans un code robuste, nous mettrions en place des méthodes de contrôle et de protection pour nous prémunir de cas aux limites et des bogues résiduels liés à ce débordement.

L’intérêt de passer en u64 est que je suis sûr que la valeur de périmètre retournée est dans la limite des valeurs possibles et que ma fonction ne provoquera pas de problème (avertissement, erreur ou valeur incohérente).

Écriture plus idiomatique

J’ai choisi l’écriture ci-avant pour une meilleure lisibilité du code. C’est un choix pédagogique et didactique.

Dans la vraie vie, nous pouvons raccourcir le code et éviter la création d’une variable inutile :

#![allow(unused)]
fn main() {
pub fn calculer_perimetre(cote: u32) -> u64 {
    (cote as u64) * 4_u64
}
}

J’utiliserai indifféremment l’une ou l’autre version dans la suite.


  1. Oui, je suis taquin et pointilleux. ↩2 ↩3

Documenter nos codes Rust

Commentaires

Dans Rust, nous pouvons ajouter des commentaires n’importe où dans une ligne avec //. Tout ce qui qui suit sur cette ligne est un commentaire.

Par exemple :

#![allow(unused)]
fn main() {
pub fn calculer_perimetre(cote: u32) -> u64 {
    // Conversion en u64 (pour éviter le débordement dans le résulat du calcul)
    let cote_u64: u64 = cote as u64;
    // périmètre = 4 fois la longueur d'un côté
    cote_u64 * 4_u64 // retour fonction
}
}

Documentation

La documentation des composants d’un projet est constituée de lignes commençant par ///. 1

Par exemple :

#![allow(unused)]
fn main() {
/// Calcule le périmètre d'un carré à partir de la longueur de son côté.
pub fn calculer_perimetre(cote: u32) -> u64 {
    (cote as u64) * 4_u64
}
}

Comme notre exemple ne le montre pas, la documentation Rust utilise la syntaxe Markdown.

C’est ce qui permet nativement la mise en forme très lisible que l’on retrouve dans les supports de https://docs.rs et dans tous les projets Rust, publics ou privés.


  1. Nous verrons plus tard comment documenter au niveau projet (crate).

Utiliser les exemples de documentation

Rust propose d’agrémenter notre documentation d’exemples d’utilisation de nos outils.

C’est une excellente pratique. Elle est grandement facilitée et rendue bien plus utile et sécurisante par l’architecture des outils Rust.

Exemples dans la documentation

L’utilisation native de Markdown permet d’insérer des blocs de codes dans la documentation, notamment pour y placer des exemples d’utilisation.

Dans notre librairie, par exemple :

#![allow(unused)]
fn main() {
/// Calcule le périmètre d'un carré à partir de la longueur de son côté.
///
/// # Exemple
///
/// ```
/// use mon_api_maths::calculer_perimetre;
///
/// let resultat: u64 = calculer_perimetre(5_u32);
/// assert_eq!(resultat, 20_u64);
/// ```
pub fn calculer_perimetre(cote: u32) -> u64 {
    (cote as u64) * 4_u64
}
}

Nous avons écrit le code source d’un petit programme exemple pour documenter notre fonction, que nous pouvons extraire et isoler :

#![allow(unused)]
fn main() {
use mon_api_maths::calculer_perimetre;

let resultat: u64 = calculer_perimetre(5_u32);
assert_eq!(resultat, 20_u64);
}

Le programme :

  • importe la fonction que nous avons écrite avec use mon_api_maths::calculer_perimetre; comme le ferait n’importe quel programme qui utiliserait notre API ;
  • crée une variable resultat et lui affecte le résultat du calcul, par notre fonction, du périmètre d’un carré de côté 5 ;
  • utilise une procédure de test pour vérifier que le résultat est bien celui attendu, 5 × 4 = 20, avec la macro assert_eq!(resultat, 20);.

À la différence d’un vrai programme, ce code n’a pas besoin de comporter de fonction fn main().

Utilisation lors des tests

Rust intègre les exemples de documentation dans la procédure de tests des librairies et des programmes.

Ainsi, si nous effectuons

cargo test

dans notre projet, nous verrons ce retour commande :

$ cargo test
   Compiling mon_api_maths v0.1.0 (/home/user/mesprojets/mon_api_maths)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.20s
     Running unittests src/lib.rs (target/debug/deps/mon_api_maths-da3e7a1947867a35)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/mon_api_maths-44a383f15f2f56d8)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests mon_api_maths

running 1 test
test src/lib.rs - calculer_perimetre (line 5) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

all doctests ran in 0.15s; merged doctests compilation took 0.15s

La partie intéressante est :

running 1 test
test src/lib.rs - calculer_perimetre (line 5) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

En réalité, Cargo a vérifié la liste des tests unitaires disponibles, a trouvé un code exemple de documentation, et l’a testé dans la partie correspondante.

Pour n’effectuer QUE les tests des exemples de la documentation, nous pouvons nous servir de la commande

cargo test --doc

Avantages

L’avantage principal d’une telle approche est de nous assurer que les exemples que nous produisons dans notre documentation sont valides lorsque nous produisons cette documentation.

Sous réserve, bien sûr,

  • d’avoir respecté tous les éléments du formalisme, c’est à dire d’avoir placé ces blocs dans une syntaxe de ce type :

    #![allow(unused)]
    fn main() {
    /// ```
    /// notre bloc de code — pas besoin de fonction main() —
    /// ```
    }
  • de réaliser nos tests unitaires.

Utiliser les tests unitaires

En complément des exemples de la documentation destinée à aux utilisateurs·trices de notre API, nous pouvons intégrer des tests unitaires plus poussés dans nos codes sources.

Ici, par exemple, nous allons ajouter un test unitaire pour vérifier le comportements aux limites que j’ai survolé dans mon code en choisissant le type u64 en sortie de fonction.

Pour cela, nous allons ajouter un module de test à la suite de notre fonction. C’est la convention en Rust : nous disposons les tests unitaires dans le même fichier que le code source testé.

Code source pour implémenter le test

#![allow(unused)]
fn main() {
/// [Documentation avec exemple…]
pub fn calculer_perimetre(cote: u32) -> u64 {
    (cote as u64) * 4_u64
}

#[cfg(test)]
mod tests {
    use super::calculer_perimetre;

    #[test]
    fn test_limite_debordement() {
        // Le nombre maximum pour un u32 est 4_294_967_295.
        // Divisé par 4, cela donne 1_073_741_823.
        // Si nous passons une longueur de côté supérieure à cette valeur, 
        // que se passe-t-il à l'exécution ?
        let resultat = calculer_perimetre(1_073_741_824);
        assert_eq!(resultat, 4_294_967_296);
    }
}
}

Description détaillée

#[cfg(test)]

C’est un attribut de compilation conditionnelle.

  • cfg signifie configuration.
  • Cet attribut indique au compilateur Rust que tout ce qui suit (ici, le module tests) ne doit être compilé et exécuté que lorsque l’on lance la commande cargo test.
  • Lorsque nous compilons notre projet pour la production avec cargo build --release par exemple, les blocs de tests sont ignorés. Cela évite d’alourdir le binaire final inutilement avec du code de tests.

mod tests { ... }

C’est la déclaration d’un sous-module nommé tests.

  • En Rust, la convention veut que l’on isole les tests unitaires dans leur propre module.
  • Ce module crée un nouvel espace de noms (namespace) étanche, ce qui évite de mélanger les fonctions de tests avec les fonctions de notre API.

use super::calculer_perimetre;

Nous importons notre fonction dans notre module de tests. super est un mot clef permettant de remonter au niveau supérieur (par rapport à notre sous-module de test).

#[test]

C’est l’attribut de test.

  • Il sert de marqueur pour le gestionnaire de tests de Rust (test runner).
  • Lorsqu’on tape cargo test, Rust parcourt le code à la recherche de cet attribut et sait qu’il doit exécuter la fonction située juste en dessous.

fn test_limite_debordement() { ... }

C’est une fonction classique qui contient le scénario de test.

  • Par convention, on lui donne un nom très descriptif (souvent préfixé par test_) pour l’identifier dans la console qui affiche les résultats de tests.
  • Les fonctions de test ne prennent jamais de paramètres et ne retournent généralement rien.

let resultat = calculer_perimetre(1_073_741_824);

C’est l’action.

  • On appelle la fonction à tester avec la valeur limite déterminée mathématiquement ($1_073_741_824$).
  • Le résultat est stocké dans la variable resultat.

assert_eq!(resultat, 4_294_967_296);

C’est la vérification (l’assertion).

  • assert_eq! est une macro (reconnaissable à son !) qui signifie assert equal (vérifier l’égalité).
  • Elle compare le premier argument (resultat) avec le second argument attendu ($4_294_967_296$).
  • Si les deux valeurs sont égales, le test passe. S’il y a la moindre différence ou si le code panique avant, le test échoue et Rust affiche explicitement la valeur obtenue face à la valeur attendue dans le retour.

Documentation

Comme pour l’ensemble de nos codes sources, nous prenons soin de documenter notre test en expliquant et explicitant tout ce qui doit l’être.

Nous le faisons avec des « commentaires » simples (//) : les tests, même s’ils complètent la documentation du code utilement pour les développeurs·ses et mainteneurs·ses de ce code, n’ont pas vocation à apparaître dans la documentation qui sert, elle, aux consommateurs·trices de notre API.

Nous pouvons remarquer et souligner que les tests unitaires constituent une part importante et pertinente de documentation des codes sources. Ils donnent des informations critiques (ici, le comportement aux limites).

Le fait que ces tests soient intégrés dans le code source AVEC ce code source me semble très utile : en tant que consommateur de codes écrits par d’autres, j’apprécie de pouvoir me faire une opinion des points d’attention que les développeurs·ses ont listés et traités (ou omis voire délaissés !) dans leurs tests unitaires.

Mise en application

Pour exécuter les tests, nous utilisons la commande que nous connaissons maintenant :

cargo test

La commande se charge de tout et retourne :

   Compiling mon_api_maths v0.1.0 (/home/user/mesprojets/mon_api_maths)
    Finished `test` profile [unoptimized + debuginfo] target(s) in 0.45s
     Running unittests src/lib.rs (target/debug/deps/mon_api_maths-da3e7a1947867a35)

running 1 test
test tests::test_limite_debordement ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

     Running unittests src/main.rs (target/debug/deps/mon_api_maths-44a383f15f2f56d8)

running 0 tests

test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

   Doc-tests mon_api_maths

running 1 test
test src/lib.rs - calculer_perimetre (line 5) ... ok

test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s

all doctests ran in 0.23s; merged doctests compilation took 0.23s

Nos retrouvons :

  • notre nouveau test unitaire : test tests::test_limite_debordement ... ok et
  • le test de l’exemple de documentation : test src/lib.rs - calculer_perimetre (line 5) ... ok

Utiliser rustfmt et clippy

Créer ou maintenir des codes sources, c’est aussi apprendre et / ou définir des règles d’écriture et nous y tenir pour obtenir un code homogène, plus facile à lire, relire et faire évoluer.

C’est enfin tenter, autant que faire se peut, d’écrire des codes succincts et conformes aux meilleures pratiques dans le langage que nous utilisons. Ici, Rust.

Vérifier et homogénéiser le formatage du code avec rustfmt

Pour vérifier le formatage dans modification, nous utilisons la commande

cargo fmt -- --check

Pour laisser rustfmt effectuer ses modifications, nous utilisons

cargo fmt

Utiliser l’analyseur statique (linter) clippy

clippy analyse la sémantique de nos codes pour y déceler des optimisations, des fautes d’inattention ou des structures non idiomatiques (comme l’utilisation abusive du mot-clé as au lieu de From, par exemple ! — Comprenne qui pourra. —).

Pour effectuer l’analyse, nous utilisons

cargo clippy

Clippy catégorise ses retours :

  • Allow : suggestions mineures ignorées par défaut.
  • Warn : avertissements sur des codes qui fonctionnent mais qui ne sont pas idiomatiques ou qui sont potentiellement dangereux (réglage par défaut).
  • Deny : erreurs strictes qui bloquent la compilation.

Nous pouvons :

  • Forcer Clippy à être plus strict en bloquant tout ce qui génère un avertissement (y compris dans le compilateur rustc) :

    cargo clippy -- -D warnings
    
  • Autoriser les corrections automatiques avec la commande :

    cargo clippy --fix
    

Outils pour travailler avec Rust

Au delà de ceux que nous avons déjà vus, nous disposons d’outils pour travailler avec Rust.

La documentation embarquée

Nous pouvons générer une documentation de notre projet avec la commande

cargo doc

Cette documentation générée localement dans target/doc/ inclut les dépendances (crates) que nous avons injectées dans notre projet (via le fichier cargo.toml).

Le fonctionnement hors ligne et l’outil de recherche sont bien pratiques pour gagner du temps lors de nos recherches liées aux outils que nous utilisons dans le projet.

Pour générer la documentation et l’ouvrir directement, utiliser

cargo doc --open

La commande rapide cargo check et l’EDI

Beaucoup d’environnements de développement intégré font du très bon travail pour nous aider avec Rust via cargo check ou un dérivé et au travers de serveurs de langage (LSP).

Il faut bien sûr installer et paramétrer les extensions correspondant à notre environnement.

Les documentations officielles et connexes

La documentation officielle du langage est très fournie : https://doc.rust-lang.org/stable/.

L’ensemble des documentations accessibles par le Web est accessible localement sur notre machine grâce à

rustup doc

qui ouvre le fichier html correspondant à https://doc.rust-lang.org/stable/ localement avec l’application que nous avons définie par défaut pour les fichiers html (généralement un navigateur web).

Le fameux Book (The Rust Programming Language), par exemple, est servi localement ce qui permet de travailler même hors connexion à l’Internet.

Bonnes pratiques en développement Rust

Pour nous résumer à l’issue de cette première partie, nous devrions, lorsque nous développons des librairies ou des programmes avec Rust :

  • Écrire des codes suffisamment explicites et « bien pensés » dès leur conception.
  • Réfléchir aux risques auxquels nous exposons les consommateurs·trices de nos codes : Bien que Rust nous protège beaucoup plus et beaucoup mieux que beaucoup d’autres langages, notre « fil rouge » illustre à merveille le fait que nous devons toujours rester vigilants. Ce que nous faisons bien à cette étape se retrouve dans la qualité de notre code ainsi que dans nos tests unitaires.
  • Documenter (et non pas commenter) nos codes :
    • en pensant en premier lieu à la documentation du programme ou de l’API ;
    • et en second rideau aux développeurs·ses (nous-même compris·e) qui devront relire ou maintenir le code.
  • Concevoir, mettre en place et vérifier régulièrement des batteries de tests unitaires, dont les exemples de documentation font partie intégrante.

Nous avons également appris que nous pouvons créer des exemples de codes externes au code source, dans un dossier dédié à cet exercice (par défaut, examples/).

Nous utilisons régulièrement  :

  • rstfmt (formateur) et
  • clippy (linter) pour assainir et homogénéiser nos codes.

Enfin, nous n’en avons pas encore beaucoup parlé puisque nos codes étaient simples et sans erreurs de compilation, mais le compilateur Rust rustc est notre meilleur allié pour nous avertir en cas de problème et nous donner des pistes d’amélioration.

En résumé

  1. Nous documentons nos codes au fur et à mesure ;

  2. Nous insérons des codes exemples dans cette documentation dès que possible ;

  3. Nous définissons et écrivons des tests unitaires dès que possible (il est souvent bénéfique d’écrire les tests avant le code lui-même).

  4. Nous effectuons cette boucle le plus souvent possible :

    • compilation (via cargo run, cargo test ou à défaut cargo buid) ;
    • formatage avec cargo fmt ;
    • analyse syntaxique avec cargo clippy et enfin
    • tests unitaires avec cargo test.

Conclusion

Plus tôt nous intégrons toutes ces étapes dans nos méthodes de travail, mieux nous nous portons durablement et apprécions l’environnement de développement natif proposé par Rust.

Chemins d’apprentissage vers Rust

Il y a plusieurs manières d’apprendre et de maîtriser Rust, et une myriades de parcours possibles en fonction de qui nous sommes et de nos parcours de vie.

Par exemple, nous pouvons apprendre Rust

  • en lisant des livres, en écoutant des conférences ou des interventions ;
  • en codant,
  • en comparant à d’autres langages (je pense que c’est une très mauvaise approche, pour ma part),
  • en réécrivant (idem),
  • en nous imprégnant,
  • en dupliquant,
  • etc.

Aucune n’est meilleure ou plus mauvaise a priori.

Par contre, nous pouvons

  • être exposé·e·s à de mauvais supports (les supports qui implémentent de réelles approches pédagogique et didactique qualitatives et progressives sont rarissimes, voire inexistants) ;
  • mal vivre ou ne pas réussir à nous adapter à l’un ou l’autre des interlocuteurs et des outils à notre disposition ;
  • apprendre de travers en nous laissant influencer par des pratiques inappropriées ou insuffisamment réfléchies et mûries (comme, par exemple, ces gens qui n’ont pas la rigueur d’esprit de distinguer des nombres entiers naturels et relatifs, voire ne savent pas ce que c’est, à l’image de l’exemple que nous avons vu avec notre fonction de calcul du périmètre d’un carré)
  • etc.

Je vais tenter ici d’explorer quelques possibilités qui s’offrent à nous et de vous proposer des parcours et approches qui reposent sur un principe simple et, selon moi, essentiel : transmettre progressivement des savoir-être (attitude, posture) et des savoir-faire (action) en travaillant sur des cas pratiques.

Nous veillerons le plus possible à proposer une progression structurée sans faire des bonds de géant qui polluent la transmission et déroutent les apprenant·e·s.

Nous utiliserons ici

  • des processus de transmission de savoirs et de savoir-faire ainsi que des
  • des processus d’amélioration continue que j’affectionne si et seulement si les uns et les autres sont là pour servir un objectif pédagogique et didactique.

Tout ceci est extrêmement complexe et difficile à réaliser sur un support comme un livre ou un site web statique, mais qui a dit que tout devrait être facile, dans la vie ?

Il ya une chose dont je suis sûr, c’est que Rust n’est ni

  • « un langage parmi d’autres » ni
  • « un langage comme les autres ou différent des autres ».

La meilleure manière de se servir d’apprendre à maîtriser Rust est de le faire en sachant que nous ne savons rien, tout en sachant des choses essentielles et qui sont gravées dans le marbre de la manière dont les machines fonctionnent. Et encore, restons prudent·e·s : l’avènement des unités de stockage et interfaces type NVMe a révolutionné bien des choses, par exemple, dans la manière dont ceux qui ont cette chance écrivent des codes informatiques de programmes opérant de gros volumes d’entrées sorties (Input/Output = I/O).

Enfin, nous conserverons un regard critique, et parfois acerbe, à l’égard de Rust comme du reste. Il faut savoir s’offusquer de la bêtise qui mène parfois à de la dette technique sinon insurmontable du moins très pénible et qui, pourtant, avec un peu plus de jugeote, auraient pu être évitée.

Il y a, par exemple, un vrai capharnaüm dans Rust avec le move qui n’a pas le même comportement en fonction du type traité. Imaginons quelques instants des voitures qui auraient une pédale d’accélérateur qui activerait le régulateur plutôt que d’accélérer en fonction du type ou de la couleur des chaussures que nous portons pour conduire. Ce type de manque de discernement existe bel et bien dans la Rust-sphere. C’est bien dommage.

À propos

mdBook

Ce site est construit avec mdBook, l’outil de documentation externe de la communauté Rust.

Auteur

Je m’appelle Marc JESTIN.

J’ai 55 ans, je réside actuellement en FRANCE à SURGÈRES (17).

Pour en savoir plus ou pour me contacter, rendez-vous sur

https://marcjestin.fr