Modules et crates#

A mesure qu’un programme grandit, l’organisation du code devient cruciale. Rust propose un système de modules qui structure le code en unités logiques, contrôle la visibilité des éléments et évite les conflits de noms. Ce système, combiné au gestionnaire de paquets Cargo, permet de construire des projets de toute taille — d’un petit utilitaire à un écosystème de bibliothèques interdépendantes.

Le système de modules#

Définition 58 (Module)

Un module est un espace de noms qui regroupe des définitions : fonctions (chapitre 4), structures (chapitre 8), énumerations (chapitre 9), constantes, traits et sous-modules. On déclare un module avec le mot-clé mod. Par défaut, tous les éléments d’un module sont privés.

mod geometrie {
    pub fn aire_rectangle(l: f64, h: f64) -> f64 { l * h }
    pub fn perimetre_rectangle(l: f64, h: f64) -> f64 { 2.0 * (l + h) }
}

println!("Aire = {}", geometrie::aire_rectangle(5.0, 3.0));
println!("Périmètre = {}", geometrie::perimetre_rectangle(5.0, 3.0));
Aire = 15
Périmètre = 16

Les modules peuvent être imbriqués pour créer des hiérarchies logiques. L’accès aux éléments se fait par un chemin qualifié : module::sous_module::element.

Chemins de modules#

Définition 59 (Chemins de modules)

Un chemin désigne un élément dans l’arbre des modules :

  • Chemin absolu : crate::module::element part de la racine de la crate courante.

  • self:: : désigne le module courant.

  • super:: : remonte au module parent, analogue à .. dans un système de fichiers.

mod cuisine {
    fn temperature_eau() -> u32 { 100 }
    pub mod recettes {
        pub fn infusion() -> String {
            let t = super::temperature_eau(); // super remonte au parent
            format!("Infuser à {t}°C")
        }
    }
}
println!("{}", cuisine::recettes::infusion());
Infuser à 100°C

Remarque 43

Le préfixe crate:: n’est utilisable que dans du code source classique (fichiers .rs). Dans l’environnement intéractif evcxr, chaque cellule est evaluée dans un contexte autonome. Dans un vrai projet, crate:: permet toujours de référencer la racine, quelle que soit la profondeur du module courant.

Visibilité#

Définition 60 (Niveaux de visibilité)

Rust offre un contrôle fin de la visibilité :

  • Privé (par défaut) : accessible uniquement dans le module courant et ses sous-modules.

  • pub : accessible depuis tout code externe.

  • pub(crate) : visible dans toute la crate, mais pas depuis les crates dépendantes.

  • pub(super) : visible depuis le module parent.

  • pub(in chemin) : visible depuis le module désigné par chemin.

Exemple 61 (Champs privés et constructeur)

La visibilité s’applique aussi aux champs des structures (chapitre 8). Une structure publique peut garder des champs privés pour imposer des invariants.

mod banque {
    pub struct Compte {
        pub titulaire: String,
        solde: f64, // privé
    }

    impl Compte {
        pub fn nouveau(nom: &str, depot: f64) -> Self {
            Compte { titulaire: String::from(nom), solde: depot }
        }
        pub fn solde(&self) -> f64 { self.solde }
        pub fn deposer(&mut self, m: f64) { if m > 0.0 { self.solde += m; } }
    }
}

let mut c = banque::Compte::nouveau("Alice", 1000.0);
c.deposer(250.0);
println!("{} : {:.2} EUR", c.titulaire, c.solde());
Alice : 1250.00 EUR

Proposition 8 (Règle de visibilité des champs)

Lorsqu’au moins un champ d’une structure est privé, il est impossible de construire l’instance directement depuis l’extérieur du module. Le module doit fournir une fonction constructrice. Cette contrainte garantit le respect des invariants.

Le mot-clé use#

Le mot-clé use importe un chemin dans la portée courante afin d’éviter de répéter le chemin complet.

mod formes {
    pub struct Cercle { pub rayon: f64 }
    impl Cercle {
        pub fn aire(&self) -> f64 { std::f64::consts::PI * self.rayon * self.rayon }
    }
}

use formes::Cercle;
let c = Cercle { rayon: 5.0 };
println!("Aire = {:.2}", c.aire());
Aire = 78.54

Renommage avec as#

use std::collections::HashMap as Dictionnaire;

let mut caps = Dictionnaire::new();
caps.insert("France", "Paris");
caps.insert("Allemagne", "Berlin");
println!("{:?}", caps);
{"France": "Paris", "Allemagne": "Berlin"}

Imports groupes et glob#

Définition 61 (Imports groupes et glob)

La syntaxe use chemin::{A, B, C} regroupe plusieurs imports. L’opérateur glob use chemin::* importe tous les éléments publics d’un module. Le glob est à utiliser avec parcimonie car il rend l’origine des noms moins explicite.

use std::collections::{BTreeMap, HashSet};

let mut ensemble = HashSet::new();
ensemble.insert(42);

let mut arbre = BTreeMap::new();
arbre.insert("a", 1);

println!("Ensemble : {:?}, Arbre : {:?}", ensemble, arbre);
Ensemble : {42}, Arbre : {"a": 1}

Remarque 44

Par convention, on importe les fonctions via leur module parent (use module::sous_module, appel sous_module::f()) et les types directement (use module::MonType). Cette convention rend l’origine des éléments explicite à la lecture.

Fichiers comme modules#

Définition 62 (Correspondance fichiers-modules)

Lorsque le compilateur rencontre mod nom; (sans bloc), il cherche le code dans :

  • nom.rs dans le même répertoire, ou

  • nom/mod.rs dans un sous-répertoire.

La première convention est recommandée depuis l’edition 2018. La seconde reste nécessaire pour les modules contenant des sous-modules dans des fichiers séparés.

Exemple 62 (Structure multi-fichiers)

Considérons un projet de bibliothèque de calcul :

// mon_projet/src/
// +-- lib.rs          mod vecteur; mod utils;
// +-- vecteur.rs      pub struct Vecteur { pub x: f64, pub y: f64 }
// +-- utils/
//     +-- mod.rs      pub mod affichage;
//     +-- affichage.rs

// --- src/lib.rs ---
mod vecteur;
mod utils;
pub use vecteur::Vecteur; // réexport public

// --- src/vecteur.rs ---
pub struct Vecteur { pub x: f64, pub y: f64 }
impl Vecteur {
    pub fn norme(&self) -> f64 { (self.x * self.x + self.y * self.y).sqrt() }
}

// --- src/utils/affichage.rs ---
use crate::vecteur::Vecteur;
pub fn afficher(v: &Vecteur) { println!("({}, {})", v.x, v.y); }

Remarque 45

La directive pub use est un réexport : les utilisateurs écrivent use mon_projet::Vecteur au lieu de use mon_projet::vecteur::Vecteur. Cette technique simplifie l’API publique sans modifier l’organisation interne.

Crates et paquets#

Définition 63 (Crate et paquet)

  • Une crate est l’unité de compilation de Rust. Il existe deux types : les crates binaires (point d’entrée fn main()) et les crates bibliothèques.

  • Un paquet (package) est un ensemble d’une ou plusieurs crates, décrit par un fichier Cargo.toml. Un paquet contient au plus une crate bibliothèque et un nombre quelconque de crates binaires.

Proposition 9 (Racines de crate)

src/main.rs est la racine de la crate binaire ; src/lib.rs celle de la crate bibliothèque. Des binaires supplémentaires peuvent être places dans src/bin/, chaque fichier constituant une crate indépendante.

Dépendances dans Cargo.toml#

Exemple 63 (Déclaration de dépendances)

Voir le code ci-dessous.

[package]
name = "mon_projet"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
rand = "0.8"

[dev-dependencies]
criterion = "0.5"

Remarque 46

La section [dev-dependencies] déclare les dépendances pour les tests et benchmarks uniquement. Elles ne sont pas incluses dans le binaire final.

Une fois la dépendance déclarée, un use suffit pour accéder à ses éléments :

use rand::Rng;

fn main() {
    let mut rng = rand::thread_rng();
    let n: u32 = rng.gen_range(1..=100);
    println!("Nombre aléatoire : {n}");
}

L’écosystème crates.io#

Définition 64 (crates.io et versions sémantiques)

crates.io est le registre officiel des paquets Rust. Chaque crate suit le versionnage semantique MAJEURE.MINEURE.CORRECTIF :

  • MAJEURE : changements incompatibles.

  • MINEURE : ajouts rétrocompatibles.

  • CORRECTIF : corrections rétrocompatibles.

rand = "0.8" dans Cargo.toml accepte toute version >= 0.8.0, < 0.9.0.

La commande cargo add simplifie la gestion des dépendances :

cargo add serde --features derive    # ajouter une dépendance
cargo add --dev criterion            # dépendance de développement
cargo add tokio@1.35 --features full # version précise

Remarque 47

Le fichier Cargo.lock enregistre les versions exactes de toutes les dépendances. Il doit être versionné pour les crates binaires (compilations reproductibles) mais est généralement exclu pour les crates bibliothèques.

Workspaces#

Définition 65 (Workspace)

Un workspace est un ensemble de paquets partageant un même target/ et un même Cargo.lock. Il permet de développer plusieurs crates interdépendantes au sein d’un même dépôt, en mutualisant les dépendances compilées.

Exemple 64 (Configuration d’un workspace)

Voir le code ci-dessous.

# Cargo.toml racine
[workspace]
members = ["core", "cli", "api"]
resolver = "2"

[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }

Chaque membre peut dépendre d’un autre par un chemin local :

# cli/Cargo.toml
[package]
name = "cli"
version = "0.1.0"
edition = "2021"

[dependencies]
core = { path = "../core" }
serde.workspace = true

Proposition 10 (Avantages du workspace)

  1. Toutes les crates membres utilisent les mêmes versions des dépendances partagées.

  2. Le répertoire target/ est mutualisé, évitant la recompilation redondante.

  3. cargo build, cargo test et cargo clippy s’appliquent à tout le workspace en une invocation.

Remarque 48

La syntaxe serde.workspace = true dans le Cargo.toml d’un membre hérite la version définie dans [workspace.dependencies]. Un seul endroit à modifier pour mettre à jour une dependance dans tout le projet.

Récapitulatif#

// Synthèse : modules, visibilité, use
mod physique {
    pub mod cinematique {
        pub fn vitesse(d: f64, t: f64) -> f64 { d / t }
    }
    pub mod dynamique {
        pub fn force(m: f64, a: f64) -> f64 { m * a }
    }
}

use physique::cinematique::vitesse;
use physique::dynamique::force;

let v = vitesse(100.0, 9.58);
let f = force(80.0, v / 9.58);
println!("Vitesse = {v:.2} m/s, Force = {f:.2} N");
Vitesse = 10.44 m/s, Force = 87.17 N

Le mot-cle mod crée un espace de noms ; les chemins crate::, self:: et super:: naviguent dans l’arbre. La visibilité est privée par défaut ; les niveaux de pub offrent un contrôle précis. use simplifie l’accès aux éléments. Chaque fichier ou dossier peut correspondre à un module. Cargo et crates.io fournissent un ecosystème de dépendances mature, et les workspaces structurent les grands projets en crates coopérantes.