Skip to content

Rust Aide-mémoire

Langage système axé sur la sécurité, la vitesse et la concurrence.

01

Bases

Variables et mutabilité

Les variables Rust sont immuables par défaut — c'est une fonctionnalité de sécurité essentielle empêchant la mutation accidentelle. Utilisez 'mut' uniquement lorsque vous avez vraiment besoin de modifier une valeur. 'const' nécessite un type explicite et est intégré au moment de la compilation, tandis que 'static' a une adresse mémoire fixe avec une durée de vie 'static.

rust
let x = 5;            // immutable by default
let mut y = 10;        // mutable with 'mut'
y += 1;                // OK, y is mutable

const MAX: u32 = 100;  // compile-time constant
static GREETING: &str = "Hi"; // global static

let z: i32 = -5;       // explicit type annotation

Masquage

Le masquage vous permet de réutiliser un nom de variable tout en modifiant son type ou sa valeur. Contrairement à 'mut', le masquage crée une nouvelle liaison — utile pour transformer des données (par exemple, analyser une chaîne en int) sans inventer un nouveau nom. L'ancienne valeur est supprimée après la nouvelle liaison.

rust
let x = 5;
let x = x * 2;         // shadow previous x, now 10
let x = "text";        // can even change type!
println!("{}", x);     // "text"

// shadowing vs mut: shadowing creates a NEW variable
// with the same name, allowing type changes

Commentaires et impression

Utilisez /// pour la documentation d'élément (affichée par 'cargo doc') et //! pour la documentation de module/crate. println! écrit sur stdout, eprintln! sur stderr. L'espace réservé {} prend en charge les spécifications de formatage : > pour aligner à droite, < pour aligner à gauche, ^ pour centrer, .N pour la précision, #b/#o/#x pour binaire/octal/hex.

rust
// Line comment
/* Block comment */
/// Doc comment (renders in rustdoc)
//! Module-level doc comment

let name = "Alice";
println!("Hello, {}!", name);        // format string
println!("{0} {1} {0}", "a", "b");   // positional args
println!("{:>5}", 42);               // right-align, width 5
println!("{:.2}", 3.14159);          // 2 decimal places: 3.14
println!("{:#b}", 0b1010);           // binary: 0b1010
eprintln!("Error to stderr");        // error output

Vue d'ensemble des types de données

Rust n'a pas de conversion de type implicite — utilisez 'as' pour les conversions. i32 est le type d'entier par défaut, f64 le type de flottant par défaut. usize est utilisé pour l'indexation et les tailles. char est une valeur scalaire Unicode complète (pas un octet), donc '🦀' est un seul char.

rust
// Scalar types
let a: i32 = 42;          // signed 32-bit integer
let b: u64 = 100;         // unsigned 64-bit
let c: f64 = 3.14;        // 64-bit float (default)
let d: bool = true;
let e: char = '🦀';        // 4-byte Unicode scalar

// isize/usize: pointer-sized (32 or 64 bit depending on arch)
let len: usize = vec![1,2,3].len();

// Compound types
let pair: (i32, &str) = (1, "hello");
let arr: [i32; 3] = [0; 3];   // [0, 0, 0]

Conversion de type et alias

La conversion 'as' est non vérifiée et peut perdre des données (par exemple, 300u16 as u8 devient 44). Pour les conversions sûres, utilisez les traits From/Into qui sont implémentés pour les conversions sans perte. Les alias de type améliorent la lisibilité sans créer de nouveaux types — utilisez 'struct NewType(i32)' pour un pattern newtype distinct.

rust
// 'as' keyword for primitive casts (may truncate)
let x: i32 = 42;
let y: f64 = x as f64;        // 42.0
let z: u8 = 300u16 as u8;     // truncated: 44

// From/Into traits for safe conversion
let s = String::from("hi");
let owned: String = "hi".into();

// Type aliases
type Kilometers = i32;
let distance: Kilometers = 5;
02

Chaînes

String vs &str

La distinction entre String (possédée, tas) et &str (tranche empruntée) est fondamentale en Rust. Utilisez String lorsque vous devez posséder/modifier/étendre le texte ; utilisez &str lorsque vous avez seulement besoin de le lire. &str peut pointer vers le tampon d'une String ou vers un littéral de chaîne dans le binaire. Préférez &str dans les paramètres de fonction pour la flexibilité.

rust
// &str: immutable string slice (borrowed)
let s1: &str = "literal";           // stored in binary
let s2: &str = &s3[..2];            // slice of a String

// String: owned, growable, heap-allocated
let mut s3: String = String::from("hello");
s3.push_str(", world");             // append
s3.push('!');                       // append char
s3.insert(0, '>');                  // insert at index
println!("{}", s3);                 // >hello, world!

// Convert: &str -> String
let owned = "literal".to_string();
let owned2 = String::from("literal");

Méthodes de String

Les méthodes de String renvoient de nouvelles Strings possédées plutôt que de modifier sur place (sauf les méthodes push_*/insert sur les Strings mutables). split() renvoie un itérateur, donc utilisez .collect() pour matérialiser. Notez que l'indexation s[0] n'est PAS autorisée sur les chaînes car les limites UTF-8 ne correspondent pas aux indices d'octets — utilisez s.chars().nth(0) à la place.

rust
let s = String::from("Hello, World");

// inspection
println!("len: {}", s.len());              // 12
println!("is_empty: {}", s.is_empty());    // false
println!("contains 'World': {}", s.contains("World")); // true
println!("starts_with 'Hello': {}", s.starts_with("Hello")); // true

// transformation
let upper = s.to_uppercase();              // HELLO, WORLD
let replaced = s.replace("o", "0");        // Hell0, W0rld
let trimmed = "  hi  ".trim().to_string(); // hi

// splitting
for word in s.split(", ") {
    println!("{}", word); // Hello / World
}
let parts: Vec<&str> = s.split_whitespace().collect();

Formatage et concaténation

L'opérateur + prend possession de la String de gauche et emprunte celle de droite (&str). C'est pourquoi s1 devient invalide après s1 + &s2. Pour chaîner plusieurs chaînes, préférez format! qui est plus lisible et ne déplace aucun opérande. concat! ne fonctionne que sur les littéraux et produit un &'static str.

rust
// format! macro creates a new String
let name = "Alice";
let msg = format!("Hi {}, you have {} messages", name, 5);

// concatenation
let s1 = String::from("Hello");
let s2 = String::from(" World");
let s3 = s1 + &s2;        // s1 moved, s2 borrowed
// s1 is now invalid!

let s4 = format!("{}{}", s2, s3); // neither moved

// concat! macro for literals
let s5 = concat!("foo", "bar"); // "foobar" at compile time

Itérer sur les caractères et les octets

Les chaînes Rust sont encodées en UTF-8, donc l'indice d'octet != l'indice de caractère. chars() itère sur les valeurs scalaires Unicode (O(n) pour décoder), bytes() itère sur les octets bruts. Le découpage avec [n..m] panique si n ou m tombe au milieu d'un caractère multi-octets. Pour l'accès au niveau des octets, convertissez en Vec<u8> via as_bytes().

rust
let s = "héllo";  // é is 2 bytes in UTF-8

// iterate over chars (Unicode scalar values)
for c in s.chars() {
    print!("{} ", c);  // h é l l o
}

// iterate over bytes
for b in s.bytes() {
    print!("{} ", b);  // 104 195 169 108 108 111
}

// get char at index (O(n) — must walk UTF-8)
let third = s.chars().nth(2); // Some('l')

// string slices must be on char boundaries
let slice = &s[0..1]; // "h" — OK
// let bad = &s[0..2]; // PANIC if mid-é!

Analyse et conversion

parse() renvoie Result car la chaîne peut ne pas être un nombre valide — gérez toujours l'erreur. La syntaxe turbofish parse::<T>() vous permet de spécifier le type en ligne. Convertir String en &str est gratuit (juste un emprunt), mais &str vers String alloue. collect() peut construire une String à partir d'un itérateur de chars.

rust
// String/str -> number
let n: i32 = "42".parse().unwrap();
let n2 = "42".parse::<i32>().unwrap(); // turbofish syntax
let f: f64 = "3.14".parse().unwrap();

// number -> String
let s = 42.to_string();
let s2 = format!("{}", 42);

// String -> &str (free, just dereference)
let owned = String::from("hi");
let borrowed: &str = &owned;

// collect chars into String
let upper: String = "hello".chars().map(|c| c.to_uppercase().next().unwrap()).collect();
03

Structures de données

Tableaux et tranches

Les tableaux [T; N] ont une taille fixe connue au moment de la compilation et vivent sur la pile. Les tranches &[T] sont des pointeurs gras (pointeur + longueur) qui empruntent une séquence contiguë — elles permettent aux fonctions d'accepter n'importe quel tableau ou vecteur sans se soucier de la taille. Utilisez des tranches dans les signatures de fonction pour la généralité.

rust
// Fixed-size array (stack allocated)
let arr: [i32; 3] = [1, 2, 3];
let zeros = [0; 5];           // [0, 0, 0, 0, 0]
println!("first: {}", arr[0]);
println!("len: {}", arr.len());

// Slice: a view into an array/vector
let slice: &[i32] = &arr[1..3];  // [2, 3]
let full: &[i32] = &arr;          // whole array
let first = &arr[..1];            // [1]

// iterating
for n in &arr {
    println!("{}", n);
}

Vecteurs (Vec<T>)

Vec<T> est le tableau extensible de Rust, soutenu par la mémoire du tas avec doublement de capacité. push/pop sont O(1) amortis ; insert/remove sont O(n) car les éléments se décalent. Utilisez .get(i) au lieu de v[i] lorsque vous voulez un accès sûr (renvoie Option). into_iter() consomme le vecteur, produisant des valeurs possédées.

rust
// growable heap array
let mut v: Vec<i32> = Vec::new();
let v2 = vec![1, 2, 3];          // macro shorthand

v.push(4);                        // append
v.pop();                          // remove last -> Option
v.insert(0, 0);                   // insert at index (O(n))
v.remove(0);                      // remove at index (O(n))
v.extend([5, 6]);                 // append multiple

// access
println!("{}", v[0]);             // panics if out of bounds
println!("{:?}", v.get(0));       // Some(&4) — safe

// iterate by value, ref, or mut ref
for n in &v { print!("{}", n); }
for n in &mut v { *n *= 2; }      // double each
let owned: Vec<i32> = v.into_iter().collect();

HashMap & BTreeMap

HashMap utilise le hachage pour un accès moyen O(1) mais n'a pas d'ordre. BTreeMap utilise un B-tree pour un accès O(log n) mais garde les clés triées. Utilisez HashMap lorsque vous avez besoin de recherches rapides ; utilisez BTreeMap lorsque vous avez besoin d'une itération ordonnée ou de requêtes par plage. entry().or_insert() est la manière idiomatique de 'upsert' — elle renvoie une référence mutable à la valeur, insérant une valeur par défaut si absente.

rust
use std::collections::HashMap;
use std::collections::BTreeMap;

// HashMap: O(1) average lookup, unordered
let mut scores: HashMap<String, i32> = HashMap::new();
scores.insert(String::from("Alice"), 10);
scores.entry("Bob".into()).or_insert(0); // insert if absent
let alice = scores.get("Alice"); // Some(&10)

// iterate (unordered)
for (name, score) in &scores {
    println!("{}: {}", name, score);
}

// BTreeMap: O(log n) lookup, sorted by key
let mut bt: BTreeMap<String, i32> = BTreeMap::new();
bt.insert("zebra".into(), 1);
bt.insert("apple".into(), 2);
// iterates in sorted order: apple, zebra

HashSet & BTreeSet

HashSet stocke des valeurs uniques avec des vérifications d'appartenance O(1). Les opérations d'ensemble (union, intersection, différence, différence_symétrique) renvoient des itérateurs. Utilisez les ensembles pour la déduplication, les tests d'appartenance et les opérations mathématiques d'ensemble. BTreeSet est l'équivalent trié soutenu par BTreeMap.

rust
use std::collections::HashSet;

let mut a: HashSet<i32> = [1, 2, 3].into_iter().collect();
let b: HashSet<i32> = [3, 4, 5].into_iter().collect();

a.insert(4);
a.remove(&1);
println!("contains 2: {}", a.contains(&2));

// set operations
let union: HashSet<_> = a.union(&b).copied().collect();
let inter: HashSet<_> = a.intersection(&b).copied().collect();
let diff: HashSet<_> = a.difference(&b).copied().collect();
let sym: HashSet<_> = a.symmetric_difference(&b).copied().collect();

Tuples et déstructuration

Les tuples regroupent un nombre fixe de valeurs de types potentiellement différents. Accédez aux champs avec .0, .1, etc. La déstructuration avec les motifs let est idiomatique. Le type unité () a une seule valeur () et est utilisé comme void dans d'autres langages. Les tuples sont couramment utilisés pour retourner plusieurs valeurs depuis des fonctions.

rust
// tuples can hold different types
let tup: (i32, f64, &str) = (42, 3.14, "hi");

// access by index
println!("{} {} {}", tup.0, tup.1, tup.2);

// destructuring
let (n, pi, s) = tup;
println!("{} {} {}", n, pi, s);

// nested
let ((a, b), c) = ((1, 2), 3);

// unit tuple (empty)
let unit: () = ();

// function returning multiple values
fn divmod(a: i32, b: i32) -> (i32, i32) {
    (a / b, a % b)
}
let (q, r) = divmod(17, 5); // (3, 2)
04

Flux de contrôle

If / Else If / Else

Contrairement à de nombreux langages, 'if' en Rust est une expression qui renvoie une valeur. Cela élimine le besoin d'un opérateur ternaire (condition ? a : b) — utilisez simplement if/else. Les deux branches doivent renvoyer le même type. La condition n'a PAS besoin de parenthèses, mais le corps doit être un bloc { }.

rust
let score = 85;

// if is an expression — returns a value
let grade = if score >= 90 {
    "A"
} else if score >= 80 {
    "B"
} else if score >= 70 {
    "C"
} else {
    "F"
};
println!("Grade: {}", grade);

// arms must return same type
// let bad = if true { 1 } else { "two" }; // ERROR!

Boucles : loop, while, for

loop crée une boucle infinie — break en sort et peut renvoyer une valeur (break value). while vérifie une condition avant chaque itération. for est la boucle la plus courante, itérant sur des plages, des tableaux, des vecteurs, des itérateurs. Utilisez 'continue' pour passer à l'itération suivante. Plages : a..b est exclusif, a..=b est inclusif.

rust
// loop: infinite, use break to exit
let mut count = 0;
let result = loop {
    count += 1;
    if count == 10 {
        break count * 2; // break with value
    }
};
println!("{}", result); // 20

// while: condition-checked loop
let mut n = 5;
while n > 0 {
    println!("{}", n);
    n -= 1;
}

// for: iterate over anything with IntoIterator
for i in 0..5 { print!("{}", i); }      // 01234
for i in (1..=3).rev() { print!("{}", i); } // 321
for ch in "hello".chars() { print!("{}", ch); }

Match (filtrage par motif)

match est la puissante construction de filtrage par motif de Rust. Il doit être exhaustif (toutes les possibilités couvertes) — utilisez _ comme attrape-tout. Les motifs prennent en charge les littéraux, les plages (..=), les motifs or (|), les liaisons et les gardes (if). match est une expression et renvoie une valeur. C'est la manière idiomatique de gérer les enums comme Option et Result.

rust
let coin = 25;
match coin {
    25 => println!("quarter"),
    10 => println!("dime"),
    5 => println!("nickel"),
    1 => println!("penny"),
    _ => println!("unknown"),  // catch-all (required)
}

// matching with bindings
let x = 3;
match x {
    1 | 2 => println!("one or two"),   // or-pattern
    3..=9 => println!("single digit"),  // range
    n if n % 2 == 0 => println!("even: {}", n), // guard
    _ => println!("other"),
}

// match is exhaustive — all cases must be covered

If Let & While Let

if let est du sucre syntaxique pour match lorsque vous ne vous souciez que d'une variante. C'est moins verbeux mais moins exhaustif que match — utilisez-le lorsque les autres cas n'ont pas d'importance. while let boucle tant que le motif correspond, couramment utilisé avec les itérateurs (next() renvoie Option). La branche else est facultative.

rust
// if let: shorthand for matching one pattern
let maybe: Option<i32> = Some(5);

// verbose: match
match maybe {
    Some(x) => println!("got {}", x),
    None => println!("nothing"),
}

// concise: if let
if let Some(x) = maybe {
    println!("got {}", x);
} else {
    println!("nothing");
}

// while let: loop while pattern matches
let mut iter = vec![1, 2, 3].into_iter();
while let Some(n) = iter.next() {
    println!("{}", n);
}

Break, Continue et étiquettes

Les étiquettes (apostrophe + nom) permettent de sortir ou de continuer des boucles externes depuis des boucles imbriquées. C'est essentiel lorsque vous devez sortir de plusieurs niveaux de boucle à la fois. Sans étiquettes, break/continue n'affectent que la boucle la plus interne. Les étiquettes sont une alternative propre aux variables de drapeau.

rust
// continue: skip to next iteration
for i in 0..10 {
    if i % 2 == 0 { continue; }
    println!("{}", i); // prints odd numbers
}

// break: exit loop
for i in 0..10 {
    if i == 5 { break; }
    println!("{}", i); // prints 0..4
}

// labeled loops (for nested break/continue)
'outer: for i in 0..3 {
    for j in 0..3 {
        if i == 1 && j == 1 {
            break 'outer; // breaks the outer loop
        }
        println!("{} {}", i, j);
    }
}
05

Fonctions et fermetures

Définition de fonctions

Les fonctions utilisent le mot-clé 'fn'. La dernière expression sans point-virgule est la valeur de retour (expression). Ajouter un point-virgule la transforme en instruction renvoyant (). Utilisez 'return' explicite uniquement pour les sorties anticipées. Le type de retour ! marque les fonctions divergentes qui ne reviennent jamais (boucles infinies, panics, sorties de processus).

rust
// basic function with return type
fn add(a: i32, b: i32) -> i32 {
    a + b  // no semicolon = expression = return value
}

// statements (with semicolon) return ()
fn greet(name: &str) {
    println!("Hi, {}", name);
    // implicit return ()
}

// explicit return
fn abs(x: i32) -> i32 {
    if x < 0 {
        return -x;  // early return needs 'return'
    }
    x  // tail expression
}

// diverging function (never returns)
fn forever() -> ! {
    loop {}
}

Paramètres et arguments

Rust n'a pas de surcharge de fonction ni de paramètres facultatifs (utilisez les génériques ou les builders à la place). Choisissez les types de paramètres avec soin : &T pour l'accès en lecture, &mut T pour l'accès en écriture, T pour le transfert de propriété. Les tranches (&[T]) sont la manière idiomatique d'accepter des séquences de longueur variable. Les arguments par défaut ne sont pas pris en charge — utilisez le pattern builder ou Option<T>.

rust
// immutable borrow
fn len(s: &String) -> usize { s.len() }

// mutable borrow
fn push(v: &mut Vec<i32>) { v.push(42); }

// take ownership
fn consume(s: String) { println!("{}", s); }

// multiple return via tuple
fn swap(a: i32, b: i32) -> (i32, i32) { (b, a) }

// variadic-ish via slices
fn sum(nums: &[i32]) -> i32 {
    nums.iter().sum()
}
println!("{}", sum(&[1, 2, 3, 4])); // 10

Fermetures

Les fermetures sont des fonctions anonymes qui peuvent capturer leur environnement. Elles sont déduites par l'usage. Les fermetures capturent par référence par défaut ; 'move' force le transfert de propriété (essentiel pour les threads). Les fermetures implémentent les traits Fn (emprunt), FnMut (emprunt mutable) ou FnOnce (consomme), leur permettant d'être passées comme paramètres de fonction.

rust
// closure syntax: |params| body
let add = |a, b| a + b;
println!("{}", add(1, 2)); // 3

// type annotations (rarely needed)
let square = |x: i32| -> i32 { x * x };

// capturing environment
let multiplier = 3;
let multiply = |x| x * multiplier; // borrows multiplier
println!("{}", multiply(5)); // 15

// move closure: takes ownership of captured vars
let name = String::from("Alice");
let greet = move || println!("Hi {}", name);
// name is now moved into greet
greet();

Fonctions d'ordre supérieur et itérateurs

Les itérateurs Rust sont paresseux — les opérations ne s'exécutent pas tant que .collect() ou une autre méthode consommatrice n'est pas appelée. Cela permet une abstraction à coût nul : le compilateur peut optimiser les méthodes d'itérateur chaînées en boucles efficaces. Méthodes courantes : map (transformer), filter (sélectionner), fold (accumuler), take (limiter), skip, enumerate, zip, flat_map.

rust
let nums = vec![1, 2, 3, 4, 5];

// map: transform each element
let doubled: Vec<i32> = nums.iter().map(|x| x * 2).collect();

// filter: keep elements matching predicate
let evens: Vec<&i32> = nums.iter().filter(|&&x| x % 2 == 0).collect();

// fold/reduce: accumulate
let sum: i32 = nums.iter().sum();              // 15
let product: i32 = nums.iter().product();       // 120
let combined = nums.iter().fold(0, |acc, x| acc + x);

// chain multiple operations (lazy!)
let result: Vec<i32> = nums.iter()
    .filter(|&&x| x > 1)
    .map(|&x| x * x)
    .collect(); // [4, 9, 16, 25]

Pointeurs de fonction et traits

fn (minuscule) est un type de pointeur de fonction — à coût nul, mais ne peut pas capturer l'environnement. Pour les fermetures qui capturent, utilisez les bornes génériques <F: Fn(...)>. Fn emprunte, FnMut emprunte de manière mutable, FnOnce consomme. Les pointeurs de fonction sont utiles pour stocker des fonctions dans des structs ou les passer à C. Les génériques avec bornes Fn sont plus flexibles et toujours à coût nul lorsqu'ils monomorphisés.

rust
// function pointer type
type MathFn = fn(i32, i32) -> i32;

fn add(a: i32, b: i32) -> i32 { a + b }
fn mul(a: i32, b: i32) -> i32 { a * b }

fn apply(f: MathFn, a: i32, b: i32) -> i32 {
    f(a, b)
}
println!("{}", apply(add, 3, 4)); // 7
println!("{}", apply(mul, 3, 4)); // 12

// generic over Fn trait (accepts closures too)
fn apply_fn<F: Fn(i32, i32) -> i32>(f: F, a: i32, b: i32) -> i32 {
    f(a, b)
}
let closure = |a, b| a - b;
println!("{}", apply_fn(closure, 10, 3)); // 7
06

Propriété et emprunt

Règles de propriété

La propriété est le système central de gestion mémoire de Rust — aucun ramasse-miettes nécessaire. Lorsque vous assignez une valeur de tas (String, Vec), la propriété se DÉPLACE et l'ancienne variable devient invalide. Les types de pile (i32, f64, bool, char, tuples de types Copy) implémentent Copy et sont dupliqués à la place. Cela élimine les bugs use-after-free et double-free au moment de la compilation.

rust
// Rule 1: Each value has ONE owner
let s1 = String::from("hello");
let s2 = s1;  // s1's ownership MOVED to s2
// println!("{}", s1); // ERROR: s1 is invalid after move

// Rule 2: When owner goes out of scope, value is dropped
{
    let s = String::from("temp");
    // s is valid here
} // s is automatically dropped (memory freed)

// Rule 3: Copy types (i32, bool, char, etc.) are copied, not moved
let a = 5;
let b = a;  // a is copied, both valid
println!("{} {}", a, b); // OK

Emprunt et références

L'emprunt vous permet d'utiliser une valeur sans en prendre propriété. &T crée une référence immuable — vous pouvez en avoir plusieurs simultanément. &mut T crée une référence mutable — mais une SEULE référence mutable OU un nombre quelconque de références immuables, jamais les deux. Cela prévient les courses de données au moment de la compilation. Les références doivent toujours pointer vers des données valides (pas de pointeurs pendants).

rust
// &T: immutable borrow (read-only, multiple allowed)
fn calc_len(s: &String) -> usize {
    s.len()
    // s goes out of scope but is NOT dropped (we don't own it)
}
let s = String::from("hello");
let len = calc_len(&s);  // borrow s, don't move it
println!("'{}' has length {}", s, len); // s still valid

// &mut T: mutable borrow (exclusive, only ONE at a time)
fn push_world(s: &mut String) {
    s.push_str(", world");
}
let mut s2 = String::from("hello");
push_world(&mut s2);
println!("{}", s2); // hello, world

Références de tranches

Les tranches sont des références vers une portion contiguë d'une collection. Ce sont des 'pointeurs gras' contenant un pointeur et une longueur. Les tranches de chaîne (&str) permettent aux fonctions d'accepter à la fois String et les littéraux de chaîne. Les tranches de tableau (&[T]) fonctionnent avec n'importe quelle séquence contiguë. Les tranches empruntent les données sous-jacentes, les empêchant d'être modifiées ou supprimées tant que la tranche existe.

rust
// string slice: &str
let s = String::from("hello world");
let hello: &str = &s[0..5];   // "hello"
let world: &str = &s[6..];    // "world"
let full: &str = &s[..];      // "hello world"

// array slice: &[T]
let arr = [1, 2, 3, 4, 5];
let mid: &[i32] = &arr[1..4]; // [2, 3, 4]

// function accepting slices (idiomatic)
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' {
            return &s[0..i];
        }
    }
    &s[..]
}

Durées de vie

Les durées de vie indiquent au compilateur combien de temps les références sont valides. L'annotation 'a ne change pas les durées de vie — elle décrit les relations. 'static est une durée de vie spéciale durant tout le programme (les littéraux de chaîne l'ont). La plupart du code utilise l'élision de durée de vie (le compilateur déduit). Vous avez besoin de durées de vie explicites lorsque : une fonction renvoie une référence, ou un struct contient une référence.

rust
// explicit lifetime annotation
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
// 'a means: the returned reference lives as long as
// the SHORTEST of x and y's lifetimes

let s1 = String::from("long string");
let s2 = String::from("hi");
let result = longest(s1.as_str(), s2.as_str());
println!("Longest: {}", result);

// struct holding references needs lifetime
struct Excerpt<'a> {
    part: &'a str,
}
let novel = String::from("call me Ishmael. years ago...");
let first_sentence = novel.split('.').next().unwrap();
let ex = Excerpt { part: first_sentence };

Pointeurs intelligents

Box<T> déplace les données vers le tas (propriétaire unique). Rc<T> permet la propriété partagée via le comptage de références (mono-thread uniquement). Arc<T> est la version thread-safe utilisant des opérations atomiques. RefCell<T> déplace la vérification d'emprunt à l'exécution, permettant la mutation via des références partagées (mutabilité intérieure). Utilisez Box pour les types récursifs, Rc/Arc pour les structures de type graphe, RefCell lorsque vous devez muter des données partagées.

rust
use std::rc::Rc;
use std::sync::Arc;
use std::cell::RefCell;

// Box<T>: heap allocation, single owner
let b = Box::new(5); // 5 lives on heap
println!("{}", b);   // dereferenced automatically

// Rc<T>: reference counting, multiple owners (single-threaded)
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a); // increments ref count
println!("count: {}", Rc::strong_count(&a)); // 2

// Arc<T>: atomic Rc, thread-safe
let arc = Arc::new(vec![1, 2, 3]);

// RefCell<T>: interior mutability (runtime borrow check)
let cell = RefCell::new(5);
*cell.borrow_mut() += 1; // mutable borrow checked at runtime
07

Structs, Enums et Traits

Définition de structs

Les structs regroupent des champs liés. Les structs à champs nommés sont les plus courants. Les structs tuples sont utiles lorsque les noms de champs n'ont pas de sens (Color, Point). Les structs unitaires n'ont pas de données et sont utilisés pour implémenter des traits. La syntaxe .. copie les champs non spécifiés depuis une autre instance. Les structs sont alloués sur la pile à moins qu'ils ne contiennent des types de tas (String, Vec, Box).

rust
// named-field struct
struct User {
    name: String,
    age: u32,
    active: bool,
}

let u = User {
    name: String::from("Alice"),
    age: 30,
    active: true,
};

// field init shorthand
let name = String::from("Bob");
let u2 = User { name, age: 25, active: true };

// update syntax (copy remaining fields from another)
let u3 = User { age: 40, ..u2 };

// tuple struct
struct Color(u8, u8, u8);
let red = Color(255, 0, 0);

// unit struct (no fields, useful for traits)
struct AlwaysEqual;

Méthodes avec impl

Les méthodes vont dans les blocs impl. &self emprunte de manière immuable, &mut self emprunte de manière mutable, self prend propriété (consomme). Les fonctions associées (sans paramètre self) sont comme des méthodes statiques — appelées avec Type::function(). Self est un alias pour le type. Plusieurs blocs impl sont autorisés, utiles pour répartir les méthodes par préoccupation ou compilation conditionnelle.

rust
struct Rectangle {
    width: f64,
    height: f64,
}

impl Rectangle {
    // associated function (constructor, no &self)
    fn new(w: f64, h: f64) -> Self {
        Rectangle { width: w, height: h }
    }

    // method (borrows self)
    fn area(&self) -> f64 {
        self.width * self.height
    }

    // mutable method
    fn scale(&mut self, factor: f64) {
        self.width *= factor;
        self.height *= factor;
    }

    // consuming method (takes ownership)
    fn into_square(self) -> Rectangle {
        let side = (self.width + self.height) / 2.0;
        Rectangle { width: side, height: side }
    }
}

let mut r = Rectangle::new(10.0, 5.0);
println!("Area: {}", r.area()); // 50
r.scale(2.0);
let sq = r.into_square(); // r consumed

Enums et filtrage par motif

Les enums en Rust sont des types de données algébriques — chaque variante peut porter des données différentes. Cela les rend bien plus puissants que les enums C. Le filtrage par motif avec match déstructure les variantes et extrait leurs données. Utilisez les enums lorsqu'une valeur peut être l'une de plusieurs formes distinctes. La macro matches! est un raccourci pour la correspondance à un seul motif renvoyant un booléen.

rust
// enum with data (algebraic data type)
enum Message {
    Quit,                          // no data
    Move { x: i32, y: i32 },       // named fields
    Write(String),                 // tuple variant
    ChangeColor(i32, i32, i32),    // tuple variant
}

// pattern matching with destructuring
fn process(msg: Message) {
    match msg {
        Message::Quit => println!("Quit"),
        Message::Move { x, y } => println!("Move to ({}, {})", x, y),
        Message::Write(text) => println!("Write: {}", text),
        Message::ChangeColor(r, g, b) => println!("RGB({}, {}, {})", r, g, b),
    }
}

process(Message::Move { x: 10, y: 20 });
process(Message::Write(String::from("hello")));

// enums can have methods too
impl Message {
    fn is_quit(&self) -> bool {
        matches!(self, Message::Quit)
    }
}

Option & Result

Option<T> remplace null — vous devez gérer explicitement le cas None, éliminant les bugs de style NullPointerException. Result<T, E> est pour les opérations qui peuvent échouer. Les deux ont des méthodes riches : map (transformer), and_then (chaîner), unwrap_or (défaut), is_some/is_ok (vérifier). L'opérateur ? sur Result propage les erreurs automatiquement. Ces deux types sont l'épine dorsale de la gestion d'erreurs de Rust.

rust
// Option<T>: Some(value) or None (replaces null)
fn find_user(id: i32) -> Option<String> {
    if id == 1 { Some(String::from("Alice")) }
    else { None }
}

let user = find_user(1);
match user {
    Some(name) => println!("Found: {}", name),
    None => println!("Not found"),
}

// convenient methods
let name = find_user(1).unwrap_or("Anonymous".into());
let upper = find_user(1).map(|n| n.to_uppercase());
let len = find_user(1).and_then(|n| Some(n.len()));

// Result<T, E>: Ok(value) or Err(error)
fn parse_num(s: &str) -> Result<i32, std::num::ParseIntError> {
    s.parse()
}

match parse_num("42") {
    Ok(n) => println!("Parsed: {}", n),
    Err(e) => println!("Error: {}", e),
}

Traits et bornes de trait

Les traits définissent un comportement partagé (comme les interfaces dans d'autres langages). Les types implémentent les traits avec 'impl Trait for Type'. Les traits peuvent avoir des implémentations de méthode par défaut. Les bornes de trait (<T: Trait>) contraignent les génériques aux types implémentant certains traits. La clause 'where' améliore la lisibilité pour les bornes complexes. Les traits permettent le polymorphisme via la répartition statique (génériques) et la répartition dynamique (objets de trait &dyn Trait).

rust
// define a trait (interface)
trait Summary {
    fn summarize(&self) -> String;

    // default method
    fn author(&self) -> String {
        String::from("Unknown")
    }
}

struct Article { title: String, content: String }

impl Summary for Article {
    fn summarize(&self) -> String {
        format!("{}: {}", self.title, self.content)
    }
    // author() uses default implementation
}

let a = Article {
    title: "Rust".into(),
    content: "Great".into(),
};
println!("{}", a.summarize());
println!("Author: {}", a.author()); // Unknown

// generic with trait bound
fn print_summary<T: Summary>(item: &T) {
    println!("{}", item.summarize());
}

// multiple bounds with +, or where clause
fn display<T: Summary + std::fmt::Display>(item: &T) {}
// or: fn display<T>(item: &T) where T: Summary + std::fmt::Display {}

Macros derive et objets de trait

#[derive(...)] implémente automatiquement les traits courants : Debug (impression de débogage), Clone (copie profonde), PartialEq/Eq (comparaison ==), Hash (pour les clés HashMap), Copy (copie sur pile au lieu de déplacement). Les objets de trait (&dyn Trait ou Box<dyn Trait>) permettent le polymorphisme à l'exécution via une vtable, à un petit coût de performance. Utilisez les génériques pour la répartition statique (à coût nul) lorsque possible, les objets de trait lorsque vous avez besoin de collections hétérogènes.

rust
// derive common traits automatically
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
struct Point {
    x: i32,
    y: i32,
}

let p1 = Point { x: 1, y: 2 };
let p2 = p1.clone();          // Clone
println!("{:?}", p1);          // Debug
println!("{}", p1 == p2);      // PartialEq -> true

// trait object: dynamic dispatch
trait Animal {
    fn sound(&self) -> String;
}

struct Dog;
struct Cat;

impl Animal for Dog { fn sound(&self) -> String { "Woof".into() } }
impl Animal for Cat { fn sound(&self) -> String { "Meow".into() } }

// Vec of trait objects (dynamic dispatch via vtable)
let animals: Vec<Box<dyn Animal>> = vec![
    Box::new(Dog),
    Box::new(Cat),
];
for a in &animals {
    println!("{}", a.sound());
}
08

Gestion d'erreurs

Result & opérateur ?

L'opérateur ? est la manière idiomatique de propager les erreurs. Sur Ok(v), il déroule vers v. Sur Err(e), il renvoie Err(e) depuis la fonction immédiatement. ? convertit également les types d'erreur via le trait From, donc les fonctions renvoyant Box<dyn Error> peuvent utiliser ? sur n'importe quel type d'erreur. Sur Option, ? renvoie None de manière anticipée. Cela rend la gestion d'erreurs concise sans sacrifier la sécurité.

rust
use std::fs;
use std::io;
use std::num::ParseIntError;

// ? propagates errors: if Err, return early; if Ok, unwrap
fn read_config(path: &str) -> Result<i32, io::Error> {
    let content = fs::read_to_string(path)?; // ? on io::Result
    Ok(content.len() as i32)
}

// ? converts error types via From
fn parse_and_read(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let content = fs::read_to_string(path)?;     // io::Error -> Box
    let n: i32 = content.trim().parse()?;         // ParseIntError -> Box
    Ok(n)
}

// ? on Option too
fn first_char(s: &str) -> Option<char> {
    s.lines().next()?.chars().next()
}

Panic vs Result

Utilisez panic! pour les erreurs non récupérables (bugs, invariants violés) — cela indique une erreur de programmation. Utilisez Result pour les échecs attendus et récupérables (entrée utilisateur, E/S de fichier, réseau). unwrap()/expect() paniquent en cas d'erreur — acceptable dans les tests, prototypes, ou lorsque vous pouvez prouver que la valeur est valide. Dans le code de production, préférez une gestion d'erreur correcte avec ? et match.

rust
// panic: unrecoverable error, crashes the program
fn divide(a: i32, b: i32) -> i32 {
    if b == 0 {
        panic!("Division by zero!"); // unwinds stack
    }
    a / b
}

// Result: recoverable error, caller decides
fn safe_divide(a: i32, b: i32) -> Result<i32, String> {
    if b == 0 {
        Err(String::from("division by zero"))
    } else {
        Ok(a / b)
    }
}

// unwrap/expect: panic on Err (use sparingly)
let n: i32 = "42".parse().unwrap();       // panics on error
let n2: i32 = "42".parse().expect("valid number"); // panic with msg

// when to panic vs Result:
// panic: bugs, invariant violations, impossible states
// Result: expected failures (file not found, parse error)

Types d'erreur personnalisés

Les types d'erreur personnalisés vous donnent une gestion d'erreur typée et structurée. Implémentez Display (lisible par l'humain) et Error (pour le chaînage de source). Implémentez From pour chaque erreur sous-jacente afin que ? convertisse automatiquement. Des bibliothèques comme thiserror (macro derive) ou anyhow (boîtes d'erreur dynamiques) réduisent le code boilerplate. Utilisez thiserror pour les bibliothèques, anyhow pour les applications.

rust
use std::fmt;
use std::error::Error;

#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    NotFound(String),
}

// implement Display (required by Error)
impl fmt::Display for AppError {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO error: {}", e),
            AppError::Parse(e) => write!(f, "Parse error: {}", e),
            AppError::NotFound(name) => write!(f, "Not found: {}", name),
        }
    }
}

// implement Error
impl Error for AppError {
    fn source(&self) -> Option<&(dyn Error + 'static)> {
        match self {
            AppError::Io(e) => Some(e),
            AppError::Parse(e) => Some(e),
            _ => None,
        }
    }
}

// From impls for ? to work automatically
impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}

Correspondance et combinaison de Results

Les combinateurs de Result permettent une gestion d'erreur de style fonctionnel sans match. map transforme la valeur Ok, map_err transforme l'erreur, and_then chaîne les opérations fallibles (flatMap). unwrap_or fournit une valeur par défaut en cas d'erreur. is_ok/is_err vérifient sans consommer. Ces méthodes rendent les chaînes de gestion d'erreur lisibles et évitent l'imbrication profonde.

rust
// match on Result
let result: Result<i32, &str> = Ok(42);
match result {
    Ok(n) => println!("Got: {}", n),
    Err(e) => println!("Error: {}", e),
}

// combinators
let r1: Result<i32, &str> = Ok(5);
let doubled = r1.map(|n| n * 2);           // Ok(10)
let mapped_err = r1.map_err(|e| format!("{}", e));

// and_then: chain fallible operations
let r2 = r1.and_then(|n| if n > 0 { Ok(n * 2) } else { Err("negative") });

// unwrap_or family
let val = r1.unwrap_or(0);        // 5 or 0
let val2 = r1.unwrap_or_default(); // 5 or T::default()
let val3 = r1.unwrap_or_else(|_| 0); // lazy default

// check variants
println!("{}", r1.is_ok());  // true
println!("{}", r1.is_err()); // false

Conversion d'erreurs avec From

L'opérateur ? utilise From pour convertir les erreurs. Box<dyn Error> implémente From pour toutes les erreurs standard, ce qui en fait un attrape-tout pratique. La crate anyhow fournit anyhow::Result qui ajoute du contexte (par exemple, .context("échec de lecture de la configuration")?). Pour les bibliothèques, définissez un enum d'erreur spécifique avec thiserror ; pour les applications, utilisez anyhow pour la simplicité.

rust
use std::fs;
use std::num::ParseIntError;

// Without From: manual conversion needed
fn manual(path: &str) -> Result<i32, String> {
    let content = fs::read_to_string(path)
        .map_err(|e| e.to_string())?;  // manual convert
    let n: i32 = content.trim().parse()
        .map_err(|e| e.to_string())?;  // manual convert
    Ok(n)
}

// With Box<dyn Error>: auto-converts via From
fn boxed(path: &str) -> Result<i32, Box<dyn std::error::Error>> {
    let content = fs::read_to_string(path)?;  // auto-converts
    let n: i32 = content.trim().parse()?;      // auto-converts
    Ok(n)
}

// anyhow::Result (from anyhow crate) is ergonomic for apps
// fn anyhow_fn() -> anyhow::Result<i32> {
//     let n: i32 = something()?; // any error auto-converted
//     Ok(n)
// }
09

Modules et crates

Système de modules

Le système de modules de Rust organise le code. 'mod' déclare un module (en ligne ou via un fichier). 'pub' rend les éléments publics (privé par défaut). 'use' crée des raccourcis vers les chemins. 'pub use' réexporte des éléments (utile pour la conception d'API). Le système de fichiers reflète l'arbre des modules : mod network peut être src/network.rs ou src/network/mod.rs. La racine de crate est lib.rs (bibliothèques) ou main.rs (binaires).

rust
// mod.rs or mod declaration in lib.rs/main.rs
// File: src/lib.rs
mod network {
    pub mod connection {
        pub fn connect() -> bool { true }
        fn disconnect() {} // private
    }
}

// use: bring paths into scope
use network::connection::connect;

// calling with full path
fn main() {
    network::connection::connect();
    connect(); // after 'use'
}

// re-export with pub use
pub use network::connection::connect as open_connection;

// nested file: src/network/connection.rs
// mod network; in lib.rs loads src/network/mod.rs
// which can have: pub mod connection;

Confidentialité et visibilité

La confidentialité dans Rust est à portée de module. Les éléments sont privés par défaut — accessibles uniquement dans leur module de définition et ses descendants. 'pub' les rend publics. 'pub(crate)' restreint au crate courant (utile pour les internes de bibliothèque). 'pub(super)' restreint au module parent. Les champs de struct ont une visibilité individuelle — un struct pub peut avoir des champs privés, nécessitant un constructeur.

rust
mod my_module {
    // private by default
    fn internal_helper() {}

    // public: accessible from outside
    pub fn public_api() {
        internal_helper(); // can call private within module
    }

    // pub(crate): visible within this crate only
    pub(crate) fn crate_wide() {}

    // pub(super): visible to parent module
    pub(super) fn parent_visible() {}

    // struct fields are private even if struct is pub
    pub struct Config {
        pub name: String,    // public field
        secret: String,      // private field
    }

    // enum variants inherit the enum's visibility
    pub enum Status {
        Active,  // public because Status is public
        Inactive,
    }
}

Instructions use et alias

Les instructions use amènent des éléments dans la portée, réduisant la verbosité des chemins. Les imports groupés (use std::io::{self, Read}) sont plus propres que plusieurs lignes. Les traits doivent être dans la portée pour utiliser leurs méthodes — c'est pourquoi vous avez parfois besoin de 'use std::io::Read' même si vous ne référencez pas Read par son nom. Les imports globaux (*) sont déconseillés sauf pour les preludes.

rust
// basic use
use std::collections::HashMap;
use std::fs::read_to_string;

// grouped use
use std::io::{self, Read, Write, BufRead};
// equivalent to:
// use std::io;
// use std::io::Read;
// use std::io::Write;
// use std::io::BufRead;

// aliasing with 'as'
use std::collections::HashMap as Map;

// glob import (use sparingly)
use std::prelude::v1::*;

// bringing trait methods into scope
use std::io::Read; // now .read() method is available
let mut f = std::fs::File::open("x")?;
let mut buf = String::new();
f.read_to_string(&mut buf)?; // Read trait method

Cargo et crates externes

Cargo est le gestionnaire de paquets et le système de construction de Rust. Les dépendances vont dans Cargo.toml sous [dependencies]. Les features activent des fonctionnalités facultatives (réduisant le temps de compilation/la taille du binaire). 'cargo add' modifie automatiquement Cargo.toml. Les crates sont publiées sur crates.io. L'édition (2021) contrôle les fonctionnalités du langage. Cargo gère la compilation, les tests, la documentation et la publication.

rust
# Cargo.toml
[package]
name = "my_app"
version = "0.1.0"
edition = "2021"

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

# in Rust code
use serde::{Serialize, Deserialize};
use rand::Rng;

#[derive(Serialize, Deserialize)]
struct User { name: String, age: u32 }

fn main() {
    let mut rng = rand::thread_rng();
    let n: i32 = rng.gen_range(1..=100);
    println!("{}", n);
}

# CLI commands:
# cargo new my_app      # create new project
# cargo build           # compile
# cargo run             # compile + run
# cargo test            # run tests
# cargo add serde       # add dependency
# cargo update          # update dependencies

Tests

Les tests utilisent l'attribut #[test]. assert_eq!/assert_ne! comparent des valeurs. #[should_panic] vérifie qu'un panic se produit. Les tests peuvent renvoyer Result pour des assertions basées sur les erreurs. #[cfg(test)] garantit que le module de tests n'est compilé que pendant les tests. Les tests unitaires vivent à côté du code ; les tests d'intégration vont dans le répertoire tests/. Exécutez avec 'cargo test'. Utilisez #[ignore] pour ignorer les tests instables.

rust
// Unit tests in same file (convention: tests module)
pub fn add(a: i32, b: i32) -> i32 { a + b }

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

    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
        assert_ne!(add(2, 3), 6);
    }

    #[test]
    fn test_with_message() {
        let result = add(1, 1);
        assert_eq!(result, 2, "Expected 2, got {}", result);
    }

    #[test]
    #[should_panic(expected = "division by zero")]
    fn test_panic() {
        panic!("division by zero");
    }

    #[test]
    fn test_result() -> Result<(), String> {
        if add(2, 2) == 4 { Ok(()) }
        else { Err(String::from("math is broken")) }
    }
}

// Integration tests: tests/integration_test.rs
// Run: cargo test
10

Concurrence et E/S de fichiers

Threads

std::thread::spawn crée des threads OS. La fermeture doit être 'move' si elle capture des variables, transférant la propriété au thread (empêchant use-after-free). join() bloque jusqu'à ce que le thread se termine, renvoyant un Result. Le système de propriété de Rust prévient les courses de données au moment de la compilation — vous ne pouvez pas partager des données mutables entre threads sans synchronisation (Arc<Mutex<T>>).

rust
use std::thread;
use std::time::Duration;

// spawn a thread
let handle = thread::spawn(|| {
    for i in 0..5 {
        println!("spawned thread: {}", i);
        thread::sleep(Duration::from_millis(10));
    }
});

// main thread continues
for i in 0..3 {
    println!("main thread: {}", i);
}

// wait for spawned thread to finish
handle.join().unwrap();

// move closure: transfer ownership to thread
let data = vec![1, 2, 3];
let h = thread::spawn(move || {
    println!("got data: {:?}", data); // data moved here
});
h.join().unwrap();

Canaux (passage de messages)

Les canaux permettent la communication entre threads via le passage de messages (la devise de Go : 'Partager la mémoire en communiquant'). mpsc permet plusieurs expéditeurs (tx.clone()) mais un seul récepteur. send() renvoie Result (Err si le récepteur est supprimé). Le récepteur implémente Iterator, donc les boucles for fonctionnent naturellement. Pour plusieurs récepteurs, utilisez crossbeam-channel ou les canaux asynchrones. Le passage de messages évite la complexité de l'état mutable partagé.

rust
use std::sync::mpsc;
use std::thread;

// mpsc: multiple producer, single consumer
let (tx, rx) = mpsc::channel();

// clone transmitter for multiple producers
let tx2 = tx.clone();

thread::spawn(move || {
    let vals = vec!["a", "b", "c"];
    for v in vals {
        tx.send(v).unwrap();
    }
});

thread::spawn(move || {
    tx2.send("from tx2").unwrap();
});

// receiver is an iterator
for received in rx {
    println!("Got: {}", received);
}

Mutex & Arc (état partagé)

Arc (Atomic Reference Counted) permet la propriété partagée entre threads (Rc thread-safe). Mutex fournit un accès exclusif — lock() bloque jusqu'à l'acquisition, renvoie un MutexGuard qui libère le verrou à la destruction. RwLock permet plusieurs lecteurs ou un écrivain. La combinaison Arc<Mutex<T>> est le pattern standard pour l'état mutable partagé. unwrap() sur lock() gère l'empoisonnement (un thread a paniqué en tenant le verrou).

rust
use std::sync::{Arc, Mutex};
use std::thread;

// Arc: thread-safe reference counting
// Mutex: mutual exclusion lock
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];

for _ in 0..10 {
    let counter = Arc::clone(&counter);
    let handle = thread::spawn(move || {
        // lock() returns MutexGuard, auto-unlocks when dropped
        let mut num = counter.lock().unwrap();
        *num += 1;
    }); // lock released here
    handles.push(handle);
}

for h in handles { h.join().unwrap(); }

println!("Result: {}", *counter.lock().unwrap()); // 10

// RwLock: multiple readers OR one writer
use std::sync::RwLock;
let data = RwLock::new(5);
let r1 = data.read().unwrap();  // shared read
let r2 = data.read().unwrap();
// let w = data.write().unwrap(); // would block until r1, r2 dropped

E/S de fichiers

fs::read_to_string est pratique pour les petits fichiers. Pour les gros fichiers, utilisez BufReader/BufWriter pour réduire les appels système. Les traits Read/Write fournissent des opérations d'octets de bas niveau. BufRead ajoute lines() et read_line() pour le texte. Gérez toujours les erreurs avec ? (les fichiers peuvent être manquants, permissions refusées, disque plein). flush() garantit que les données tamponnées atteignent l'OS (mais pas nécessairement le disque).

rust
use std::fs;
use std::io::{Read, Write, BufReader, BufWriter};
use std::fs::File;

// read entire file
let content = fs::read_to_string("file.txt")?;
fs::write("output.txt", "Hello")?;

// buffered read (efficient for large files)
let file = File::open("file.txt")?;
let mut reader = BufReader::new(file);
let mut buf = String::new();
reader.read_to_string(&mut buf)?;

// line by line
use std::io::BufRead;
let file = File::open("file.txt")?;
for line in BufReader::new(file).lines() {
    println!("{}", line?);
}

// buffered write
let file = File::create("out.txt")?;
let mut writer = BufWriter::new(file);
writeln!(writer, "Line 1")?;
writeln!(writer, "Line 2")?;
writer.flush()?; // ensure written to disk

Async/Await (Tokio)

Async/await permet une concurrence efficace sans threads OS — les tâches s'exécutent sur un pool de threads et cèdent aux points .await. tokio est le runtime asynchrone le plus populaire. async fn renvoie un Future qui doit être .awaited. tokio::join! exécute les futures de manière concurrente et attend toutes. tokio::select! fait courir les futures. Async est idéal pour le travail lié aux E/S (réseau, fichiers) ; utilisez des threads pour le travail lié au CPU. Le .await ne bloque pas le thread — il rend le contrôle au runtime.

rust
use tokio::{fs, task, time};
use std::time::Duration;

// async fn returns a Future
async fn fetch_data() -> String {
    time::sleep(Duration::from_secs(1)).await;
    String::from("data")
}

// spawn concurrent tasks
#[tokio::main]
async fn main() {
    // run tasks concurrently
    let (a, b, c) = tokio::join!(
        fetch_data(),
        fetch_data(),
        fetch_data()
    );
    println!("{} {} {}", a, b, c);

    // spawn a background task
    let handle = tokio::spawn(async {
        let data = fs::read_to_string("file.txt").await.unwrap();
        println!("Read {} bytes", data.len());
    });
    handle.await.unwrap();

    // select: first to complete wins
    tokio::select! {
        val = fetch_data() => println!("Got: {}", val),
        _ = time::sleep(Duration::from_millis(500)) => {
            println!("Timeout!");
        }
    }
}
11

Approfondissement des durées de vie

Durées de vie nommées dans les fonctions

Les durées de vie sont la manière dont le compilateur suit la validité des références. Le 'a est un paramètre de durée de vie générique — il ne change pas le comportement à l'exécution, seulement la vérification à la compilation. Lorsqu'une fonction prend plusieurs références et en renvoie une, vous devez annoter les durées de vie pour que le compilateur sache que la référence renvoyée ne survivra pas à ses entrées. Cela prévient les pointeurs pendants au moment de la compilation.

rust
// Lifetimes tell the compiler how long references live
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
    if x.len() > y.len() { x } else { y }
}
// 'a means: the returned reference lives as long as
// the SHORTEST of x and y

let s1 = String::from("long string");
let result;
{
    let s2 = String::from("hi");
    result = longest(s1.as_str(), s2.as_str());
    // result valid here
    println!("{}", result);
}
// result NOT valid here — s2 is dropped

Règles d'élision de durée de vie

Les règles d'élision de durée de vie vous permettent d'omettre les annotations explicites dans les cas courants. La règle 1 assigne une durée de vie distincte à chaque paramètre de référence. La règle 2 assigne cette durée de vie à la sortie s'il y a exactement une référence d'entrée. La règle 3 s'applique aux méthodes — la sortie obtient la durée de vie de &self. Lorsqu'aucune de ces règles ne résout toutes les références, vous devez écrire des durées de vie explicites. La plupart du code Rust idiomatique a rarement besoin d'annotations explicites.

rust
// The compiler applies 3 elision rules automatically:
// 1. Each input reference gets its own lifetime
// 2. If one input lifetime, output gets that lifetime
// 3. If &self/&mut self, output gets self's lifetime

// These DON'T need explicit annotations (rule 2):
fn first_word(s: &str) -> &str {
    let bytes = s.as_bytes();
    for (i, &byte) in bytes.iter().enumerate() {
        if byte == b' ' { return &s[0..i]; }
    }
    &s[..]
}

// This NEEDS annotation (multiple inputs, rule doesn't apply):
// fn longest<'a>(x: &'a str, y: &'a str) -> &'a str

La durée de vie 'static

'static est la durée de vie la plus longue — elle dure pour toute la durée du programme. Tous les littéraux de chaîne ont cette durée de vie car ils sont intégrés au binaire. Lorsque vous voyez T: 'static comme borne, cela ne signifie pas que T doit vivre pour toujours — cela signifie que T ne doit contenir aucune référence plus courte que 'static (c'est-à-dire que les types possédés comme String, Vec, i64 satisfont toujours cela). Les threads nécessitent 'static car ils peuvent survivre à la fonction appelante.

rust
// 'static means the reference lives for the entire program
let s: &'static str = "I live forever";
// All string literals are &'static str

// Static variables (global, program-wide)
static COUNTER: AtomicUsize = AtomicUsize::new(0);
COUNTER.fetch_add(1, Ordering::SeqCst);

// 'static in bounds: T: 'static means T contains no
// non-static references (owned data always satisfies this)
fn spawn_thread<T: Send + 'static>(t: T) {
    std::thread::spawn(move || {
        drop(t);
    });
}

Durées de vie dans les structs

Lorsqu'un struct contient une référence (pas un type possédé), il a besoin d'un paramètre de durée de vie pour déclarer combien de temps cette référence est valide. L'instance du struct ne peut pas survivre aux données qu'elle emprunte. C'est courant pour les analyseurs zero-copy, les itérateurs sur des données empruntées et les vues. La durée de vie doit être déclarée à la fois sur le struct et son bloc impl. Préférez posséder les données (String, Vec) sauf si vous avez une raison spécifique d'emprunter.

rust
// Structs holding references need lifetime annotations
struct Parser<'a> {
    text: &'a str,
    pos: usize,
}

impl<'a> Parser<'a> {
    fn new(text: &'a str) -> Self {
        Parser { text, pos: 0 }
    }
    fn peek(&self) -> Option<char> {
        self.text[self.pos..].chars().next()
    }
    fn advance(&mut self) {
        self.pos += 1;
    }
}

// The struct cannot outlive the text it borrows
let text = String::from("hello");
let mut p = Parser::new(&text);
p.advance();

Durées de vie multiples et sous-typage

Les fonctions avec plusieurs références qui interagissent différemment nécessitent plusieurs paramètres de durée de vie. Le sous-typage de durée de vie signifie qu'une durée de vie plus longue peut se substituer à une plus courte (covariance) — &'static peut être utilisé partout où &'a est attendu. C'est pourquoi 'static est un sous-type de toutes les durées de vie. Utilisez plusieurs durées de vie lorsque la durée de vie de la sortie ne dépend que de certaines entrées, donnant au compilateur plus de flexibilité et à l'appelant moins de contraintes.

rust
// Multiple lifetime parameters
fn parse<'src, 'ctx>(src: &'src str, ctx: &'ctx Context) -> &'src str {
    // returns something tied to src, not ctx
    src
}

// 'static: 'a is always true (static outlives everything)
fn static_ref<'a>(_: &'a str) -> &'static str {
    "constant"  // 'static coerces to 'a
}

// Variance: &'long can be used where &'short is expected
// (covariance) — longer lifetime is a subtype
fn use_ref<'short>(r: &'short str) {}
let long: &'static str = "hi";
use_ref(long);  // OK: 'static coerces to any 'short
12

Pointeurs intelligents (Box, Rc, Arc, RefCell)

Box<T> — allocation sur le tas

Box<T> est le pointeur intelligent le plus simple de Rust — il alloue une valeur sur le tas avec propriété unique. Utilisez-le pour les types récursifs (dont la taille ne peut pas être connue à la compilation), les grandes valeurs que vous ne voulez pas déplacer sur la pile, et les objets de trait (Box<dyn Trait>) pour la répartition dynamique. Box se déréférence vers T donc il se comporte comme la valeur interne. Il a essentiellement un coût nul au-delà de l'allocation sur le tas elle-même.

rust
// Box moves data to the heap (single owner)
let b = Box::new(5);
println!("{}", b);  // derefs automatically

// Recursive types NEED Box (size unknown at compile time)
enum List {
    Cons(i32, Box<List>),
    Nil,
}
let list = List::Cons(1, Box::new(List::Cons(2, Box::new(List::Nil))));

// Trait objects (dynamic dispatch)
let shapes: Vec<Box<dyn Draw>> = vec![
    Box::new(Circle { radius: 1.0 }),
    Box::new(Square { side: 2.0 }),
];
for s in &shapes { s.draw(); }

// Box has zero runtime overhead when dereferencing

Rc<T> — comptage de références (mono-thread)

Rc<T> (Reference Counted) permet la propriété multiple pour les scénarios mono-thread. Rc::clone incrémente le compteur de références plutôt que de copier les données — copie de pointeur peu coûteuse. La valeur est supprimée lorsque le dernier Rc est supprimé. Utilisez Rc pour les nœuds de graphe partagés, les arbres parent-enfant, ou toute structure où plusieurs parties doivent posséder les mêmes données. Rc n'est PAS thread-safe — utilisez Arc pour le multithreading. Rc est immuable — vous ne pouvez pas muter à travers lui directement.

rust
use std::rc::Rc;

// Rc allows multiple owners via reference counting
let a = Rc::new(String::from("shared"));
let b = Rc::clone(&a);  // increments count, doesn't copy
let c = Rc::clone(&a);

println!("count = {}", Rc::strong_count(&a));  // 3
// Data dropped when count reaches 0

// Shared graph/tree structures
struct Node {
    children: Vec<Rc<Node>>,
    value: i32,
}
let leaf = Rc::new(Node { children: vec![], value: 1 });
let branch = Rc::new(Node {
    children: vec![Rc::clone(&leaf)],
    value: 2,
});

Arc<T> — comptage de références atomique (thread-safe)

Arc<T> (Atomically Reference Counted) est la contrepartie thread-safe de Rc. Il utilise des opérations atomiques pour le comptage de références, le rendant sûr à partager entre threads. Le compromis est un léger surcoût par rapport à Rc. Utilisez Arc chaque fois que vous avez besoin de propriété partagée entre plusieurs threads. Pour muter des données partagées, combinez Arc avec Mutex (pour accès exclusif) ou RwLock (pour accès en lecture intensif). Arc::clone est peu coûteux — il incrémente juste un compteur atomique.

rust
use std::sync::Arc;
use std::thread;

// Arc: thread-safe version of Rc (atomic ref counting)
let data = Arc::new(vec![1, 2, 3, 4, 5]);

let handles: Vec<_> = (0..3).map(|i| {
    let data = Arc::clone(&data);  // atomic increment
    thread::spawn(move || {
        println!("Thread {} sees: {:?}", i, *data);
    })
}).collect();

for h in handles { h.join().unwrap(); }

// Arc is slightly slower than Rc due to atomic operations
// Only use Arc when actually sharing across threads

RefCell<T> — mutabilité intérieure

RefCell<T> fournit la mutabilité intérieure — vous pouvez muter via une référence partagée, avec les règles d'emprunt appliquées à l'exécution au lieu de la compilation. borrow() renvoie une référence immuable, borrow_mut() une référence mutable. Violer les règles (par exemple, deux emprunts mutables) provoque une panic à l'exécution. Utilisez RefCell lorsque le compilateur ne peut pas prouver la sécurité d'emprunt (par exemple, structures de graphe, objets mock dans les tests). Rc<RefCell<T>> est le pattern classique pour les données partagées mutables mono-thread.

rust
use std::cell::RefCell;

// RefCell moves borrow checking to RUNTIME
let cell = RefCell::new(vec![1, 2, 3]);

// Multiple immutable borrows OR one mutable borrow
{
    let mut borrowed = cell.borrow_mut();
    borrowed.push(4);
}  // borrow released here

{
    let r1 = cell.borrow();     // OK
    let r2 = cell.borrow();     // OK — multiple immutable
    println!("{:?} {:?}", r1, r2);
}

// cell.borrow_mut() while r1 alive → PANIC at runtime
// Combine with Rc: Rc<RefCell<T>> for mutable shared graphs

Weak<T> — briser les cycles de références

Weak<T> est une référence non propriétaire qui n'affecte pas le compteur de références fortes. C'est essentiel pour briser les cycles de références : si un parent possède des enfants (Rc) et les enfants possèdent le parent (Rc), aucun ne sera jamais libéré — une fuite de mémoire. La solution est de rendre la référence arrière Weak. upgrade() renvoie Option<Rc<T>> — None si la valeur a déjà été supprimée. Utilisez Weak pour les liens enfant→parent, les caches et les patterns d'observateur où vous ne voulez pas maintenir les données en vie.

rust
use std::rc::{Rc, Weak, RefCell};

// Weak references don't contribute to the strong count
// — prevents memory leaks in cycles
struct Node {
    parent: RefCell<Weak<Node>>,        // weak: child → parent
    children: RefCell<Vec<Rc<Node>>>,   // strong: parent → children
}

let leaf = Rc::new(Node {
    parent: RefCell::new(Weak::new()),
    children: RefCell::new(vec![]),
});
let branch = Rc::new(Node {
    parent: RefCell::new(Weak::new()),
    children: RefCell::new(vec![Rc::clone(&leaf)]),
});
*leaf.parent.borrow_mut() = Rc::downgrade(&branch);

// Upgrade returns Option — parent may already be dropped
if let Some(p) = leaf.parent.borrow().upgrade() {
    println!("parent exists");
}
13

Objets de trait et répartition dynamique

dyn Trait — répartition dynamique

dyn Trait permet la répartition dynamique — le type concret est effacé au moment de la compilation et les appels de méthode passent par une vtable à l'exécution. Cela vous permet de stocker des types hétérogènes dans une seule collection (Vec<Box<dyn Animal>>). Le compromis : un petit coût à l'exécution (indirection de vtable, pas d'inlining) et le type ne peut pas être connu à la compilation. Utilisez les objets de trait lorsque l'ensemble des types concrets n'est pas connu à la compilation ou lorsque vous devez regrouper différents types.

rust
trait Animal {
    fn name(&self) -> &str;
    fn sound(&self) -> String;
}

struct Dog { name: String }
struct Cat { name: String }

impl Animal for Dog {
    fn name(&self) -> &str { &self.name }
    fn sound(&self) -> String { "Woof".into() }
}
impl Animal for Cat {
    fn name(&self) -> &str { &self.name }
    fn sound(&self) -> String { "Meow".into() }
}

// dyn Trait = erased type, runtime dispatch via vtable
let animals: Vec<Box<dyn Animal>> = vec![
    Box::new(Dog { name: "Rex".into() }),
    Box::new(Cat { name: "Whiskers".into() }),
];
for a in &animals {
    println!("{} says {}", a.name(), a.sound());
}

Règles de sécurité d'objet

Un trait est object-safe uniquement si le compilateur peut construire une vtable pour lui. Deux règles : (1) les méthodes ne doivent pas renvoyer Self (le type concret est effacé, donc il ne peut pas être connu), et (2) les méthodes ne doivent pas avoir de paramètres de type générique (la vtable aurait besoin d'une entrée pour chaque type possible). Les traits avec Sized comme supertrait ne sont pas non plus object-safe. Si vous avez besoin de sécurité d'objet, refactorisez les méthodes renvoyant Self pour renvoyer Box<dyn Trait> ou utilisez une fonction de fabrique séparée.

rust
// Object-safe traits CAN be used as dyn Trait:
trait Draw {
    fn draw(&self);  // OK: &self, no generics
}

// NOT object-safe:
trait Bad {
    fn create() -> Self;        // returns Self — needs known type
    fn process<T>(&self, x: T); // generic method — vtable can't cover all T
    const SIZE: usize;          // associated const (sometimes OK)
}

// Workaround for Self returns — use a factory or Box<Self>
trait GoodFactory {
    fn new_boxed() -> Box<dyn GoodFactory>;
}

// Sized bound makes trait non-object-safe
// trait Foo: Sized {}  // NOT object-safe

Objets de trait vs génériques

Les génériques utilisent la monomorphisation — le compilateur génère une copie séparée de la fonction pour chaque type concret, permettant la répartition statique et une optimisation complète (inlining). Cela n'a aucun coût à l'exécution mais augmente la taille du binaire. Les objets de trait (dyn) utilisent une seule fonction avec recherche vtable — binaire plus petit mais un petit coût à l'exécution par appel. Choisissez les génériques lorsque la performance compte et que l'ensemble de types est petit/connu ; choisissez dyn lorsque vous avez besoin de collections hétérogènes ou ne connaissez pas tous les types à l'avance.

rust
// Generics: monomorphization, static dispatch, zero overhead
fn max_generic<T: PartialOrd>(a: T, b: T) -> T {
    if a > b { a } else { b }
}
// Each type instantiation creates a separate function:
// max_generic::<i32>, max_generic::<f64>, etc.

// Trait objects: dynamic dispatch, single function
fn max_dyn(a: &dyn PartialOrd, b: &dyn PartialOrd) -> bool {
    // can't return — don't know size at compile time
    a.partial_cmp(b) == Some(std::cmp::Ordering::Less)
}

// Rule of thumb:
// - Few types, performance-critical → generics (static dispatch)
// - Many/unknown types, flexibility needed → dyn (dynamic dispatch)
// - Heterogeneous collections → must use dyn

Trait Any et downcasting

Le trait Any vous permet de stocker des valeurs de n'importe quel type et de récupérer le type concret à l'exécution via downcasting. downcast_ref::<T>() renvoie Option<&T>, downcast::<T>() renvoie Result<Box<T>, Box<dyn Any>>. C'est la porte de sortie de Rust pour lorsque vous ne connaissez vraiment pas le type à la compilation (systèmes de plugins, configurations dynamiques). Cependant, préférez les enums lorsque l'ensemble des types possibles est connu — ils sont plus sûrs, plus rapides et plus idiomatiques. Any repose sur TypeId, qui est implémenté pour tous les types 'static.

rust
use std::any::Any;

// Any enables runtime type checking & downcasting
let x: Box<dyn Any> = Box::new(42i32);

// downcast_ref returns Option<&T>
if let Some(n) = x.downcast_ref::<i32>() {
    println!("It's an i32: {}", n);
}

// downcast returns Option<T> (for Box)
let boxed: Box<dyn Any> = Box::new("hello");
if let Ok(s) = boxed.downcast::<&str>() {
    println!("Recovered: {}", s);
}

// Useful for: plugin systems, error types, heterogeneous storage
// Avoid overusing — prefer enums when the type set is known

Méthodes de trait par défaut et supertraits

Les traits peuvent fournir des implémentations de méthode par défaut que les implémenteurs peuvent remplacer ou utiliser telles quelles. Les supertraits (trait Named: Shape) exigent que le type implémenteur implémente également le supertrait — cela crée une hiérarchie où les types Named sont garantis d'avoir area() et describe(). Les méthodes par défaut réduisent le code boilerplate et permettent le pattern 'méthode d'extension' où l'ajout d'une méthode à un trait bénéficie automatiquement à tous les implémenteurs existants sans les casser.

rust
trait Shape {
    fn area(&self) -> f64;
    // Default method — can be overridden
    fn describe(&self) -> String {
        format!("Shape with area {:.2}", self.area())
    }
}

// Supertrait: trait that requires another trait
trait Named: Shape {
    fn name(&self) -> &str;
}

struct Circle { radius: f64 }
impl Shape for Circle {
    fn area(&self) -> f64 { std::f64::consts::PI * self.radius.powi(2) }
}
impl Named for Circle {
    fn name(&self) -> &str { "Circle" }
}

let c = Circle { radius: 2.0 };
println!("{}", c.describe());  // uses default method
14

Macros (déclaratives et procédurales)

Declarative Macros (macro_rules!)

macro_rules! crée des macros déclaratives qui se développent à la compilation via la correspondance de motifs. La syntaxe $(...),* est un matcher de répétition — il correspond à zéro ou plusieurs expressions séparées par des virgules. $x:expr signifie « correspondre à n'importe quelle expression et la lier à x ». Les macros sont développées avant la vérification de type, elles peuvent donc générer du code qui fonctionne avec n'importe quel type. Utilisez les macros pour réduire le code boilerplate que les génériques ne peuvent pas gérer (par exemple, arguments variadiques, extension de syntaxe).

rust
// macro_rules! defines pattern-matching macros
macro_rules! vec_of {
    ($($x:expr),*) => {{
        let mut v = Vec::new();
        $( v.push($x); )*
        v
    }};
}

let nums = vec_of!(1, 2, 3, 4);

// Recursive macro: build a HashMap
macro_rules! hashmap {
    ($($k:expr => $v:expr),*) => {{
        let mut m = std::collections::HashMap::new();
        $( m.insert($k, $v); )*
        m
    }};
}
let config = hashmap!("host" => "localhost", "port" => 8080);

Types de fragments de macro

Les spécificateurs de fragment déterminent quel type de syntaxe un argument de macro correspond. :ident correspond aux identifiants (noms), :expr correspond aux expressions (valeurs), :ty correspond aux types, :block correspond aux blocs délimités par des accolades, :stmt correspond aux instructions, :literal correspond aux littéraux. Choisir le bon spécificateur est important — :expr est le plus courant mais :ident est nécessaire lorsque vous voulez créer un nom de fonction/variable. Le système de macros est hygiénique : les identifiants introduits par les macros n'entrent pas en collision avec le code environnant.

rust
// Common fragment specifiers:
macro_rules! build_fn {
    // $name:ident — identifier (function/variable name)
    // $body:block — a block { ... }
    // $ty:ty — a type
    // $expr:expr — an expression
    // $stmt:stmt — a statement
    // $lit:literal — a literal (string, number)
    ($name:ident, $ret:ty, $body:block) => {
        fn $name() -> $ret $body
    };
}

build_fn!(get_answer, i32, { 42 });
println!("{}", get_answer());

// :pat — pattern, :path — module path
// :meta — attribute meta-item, :vis — visibility

Macros standard intégrées

Rust est livré avec de nombreuses macros intégrées. println!/eprintln! impriment sur stdout/stderr. dbg! imprime la valeur d'une expression avec les infos de fichier/ligne — excellent pour le débogage (elle renvoie aussi la valeur). assert!/assert_eq!/assert_ne! sont pour les tests et les invariants. todo!/unimplemented! marquent le code incomplet avec un panic. file!/line!/module! donnent des infos de localisation à la compilation. env!/option_env! lisent les variables d'environnement à la compilation — utiles pour intégrer des infos de version.

rust
// Formatting & printing
println!("x = {}, y = {:?}", 1, "two");
eprintln!("Error: {}", "oops");     // stderr
format!("{}-{}", "a", "b");          // returns String

// Debug helpers
dbg!(2 + 2);                          // prints [src.rs:1] 2 + 2 = 4
assert!(1 + 1 == 2);                  // panic if false
assert_eq!(2 + 2, 4);                 // panic if not equal
assert_ne!(1, 2);                     // panic if equal

// Code generation
todo!("not implemented yet");         // unimplemented!()
unimplemented!();
panic!("fatal: {}", "reason");

// Environment & file info
println!("{}:{}", file!(), line!());  // src.rs:1
println!("{}", env!("CARGO_PKG_NAME"));

Vue d'ensemble des macros procédurales

Les macros procédurales (proc macros) sont des fonctions Rust qui prennent des TokenStreams en entrée et produisent des TokenStreams en sortie — transformation complète de code à code. Contrairement aux macros déclaratives, elles peuvent faire des calculs arbitraires. Trois types : macros derive (ajoutent des implémentations de trait via #[derive]), macros d'attribut (annotent des éléments) et macros de type fonction (syntaxe personnalisée comme sqlx::query!). Elles doivent vivre dans une crate séparée avec proc-macro = true. La crate syn parse la syntaxe Rust, quote! génère le code. Exemples populaires : serde, tokio, thiserror.

rust
// Procedural macros are Rust functions that transform code
// Three kinds (must be in a separate crate with proc-macro=true):

// 1. Derive macros — #[derive(MyTrait)]
#[derive(Debug, Clone, MyTrait)]
struct Point { x: f64, y: f64 }

// 2. Attribute macros — #[my_attr]
#[my_attr]
fn function() {}

// 3. Function-like macros — my_macro!(...)
sqlx::query!("SELECT * FROM users");

// Cargo.toml for a proc-macro crate:
// [lib]
// proc-macro = true
//
// [dependencies]
// syn = "2.0"   # parse Rust code
// quote = "1.0" # generate code
// proc-macro2 = "1.0"

Hygiène des macros & motifs courants

Les macros Rust sont hygiéniques — les identifiants créés à l'intérieur d'une macro existent dans un « contexte de syntaxe » séparé et ne captureront pas accidentellement ou ne masqueront pas les variables de la portée appelante. Cela évite des bugs subtils où un nom de variable interne à la macro entre en collision avec celui de l'appelant. stringify! convertit n'importe quel flux de tokens en une chaîne littérale à la compilation (utile pour les messages d'erreur). cfg_debug! montre un motif courant : compiler conditionnellement du code basé sur la configuration de build en utilisant le système cfg!.

rust
// Hygiene: macro-introduced identifiers don't leak
macro_rules! using_temp {
    ($e:expr) => {
        let temp = $e;  // this 'temp' is distinct from caller's
        println!("{}", temp);
    };
}
let temp = 10;
using_temp!(temp + 5);  // no conflict — hygienic

// Conditional compilation macro
macro_rules! cfg_debug {
    ($($e:tt)*) => {
        #[cfg(debug_assertions)]
        { $($e)* }
    };
}
cfg_debug! {
    println!("Debug mode on");
}

// stringify! converts tokens to a string literal
let s = stringify!(a + b * c);  // "a + b * c"
15

Rust non sécurisé (Unsafe)

Pointeurs bruts

Les pointeurs bruts (*const T, *mut T) sont l'échappatoire de Rust au borrow checker. Contrairement aux références, ils peuvent être null, peuvent s'aliaser (plusieurs pointeurs vers les mêmes données) et ne suivent pas les durées de vie. Les créer est sûr, mais les déréférencer nécessite unsafe car le compilateur ne peut pas garantir leur validité. Utilisez les pointeurs bruts pour le FFI (interfaçage avec C), l'implémentation de structures de données de bas niveau (listes chaînées, vecteurs) et le code critique en performance où vous garantissez manuellement la sécurité. Documentez toujours pourquoi unsafe est valide.

rust
// Raw pointers: *const T (immutable) and *mut T (mutable)
let x = 42;
let r1: *const i32 = &x;       // coerce from reference
let r2: *mut i32 = x as *mut i32;  // cast

// Can be null, can alias, no borrow checking
let null: *const i32 = std::ptr::null();

// Creating raw pointers is safe, but DEREFERENCING is unsafe
unsafe {
    println!("r1 = {}", *r1);
}

// Convert between types (transmute-like)
let bytes: [u8; 4] = [0x78, 0x56, 0x34, 0x12];
let ptr = bytes.as_ptr() as *const u32;
unsafe { println!("0x{:x}", *ptr); }  // little-endian int

Blocs & fonctions unsafe

unsafe ne désactive pas le borrow checker — il vous permet de faire cinq choses spécifiques : (1) déréférencer des pointeurs bruts, (2) appeler des fonctions unsafe, (3) implémenter des traits unsafe, (4) accéder/muter des static mut, (5) accéder aux champs d'unions. Les blocs unsafe rendent les opérations unsafe explicites et localisées. unsafe fn déclare que l'appel de la fonction nécessite de maintenir des invariants que le compilateur ne peut pas vérifier. get_unchecked saute la vérification de bornes pour la performance — seulement sûr si vous avez vérifié l'index. Minimisez la surface unsafe et encapsulez-la derrière une API sûre.

rust
// unsafe block: a localized unsafe region
let ptr: *const i32 = &42;
let val = unsafe { *ptr };

// unsafe fn: the ENTIRE function body is unsafe
unsafe fn dangerous(ptr: *const u8) -> u8 {
    *ptr
}
// Callers must use unsafe block:
let b = 5u8;
unsafe { dangerous(&b) };

// Splitting borrows safely (compiler is conservative)
let mut v = vec![1, 2, 3, 4];
let len = v.len();
unsafe {
    let first = v.get_unchecked(0);   // no bounds check
    let last = v.get_unchecked(len - 1);
    println!("{} {}", first, last);
}

FFI — Appeler des fonctions C

Le FFI (Foreign Function Interface) permet à Rust d'appeler des fonctions C et vice versa. Les blocs extern "C" déclarent des fonctions C externes — les appeler est unsafe car le compilateur ne peut pas vérifier leur comportement. #[no_mangle] empêche Rust de renommer la fonction pour que C puisse la trouver par nom. #[repr(C)] garantit que la disposition de la structure correspond à celle de C (Rust peut réordonner les champs par défaut pour l'efficacité). Utilisez le FFI pour les appels système, les bibliothèques legacy et les bindings critiques en performance. La crate bindgen génère automatiquement les déclarations FFI à partir des en-têtes C.

rust
// extern "C" declares foreign functions
extern "C" {
    fn abs(x: i32) -> i32;
}

fn main() {
    let x = -5;
    let positive = unsafe { abs(x) };
    println!("{}", positive);  // 5
}

// Exporting Rust functions to C
#[no_mangle]  // prevent name mangling
pub extern "C" fn add(a: i64, b: i64) -> i64 {
    a + b
}

// C-compatible struct
#[repr(C)]
struct Point { x: f64, y: f64 }

Implémenter des traits unsafe

Les traits unsafe (comme Send, Sync) nécessitent que l'implémenteur maintienne des invariants que le compilateur ne peut pas vérifier. Send signifie qu'un type peut se déplacer en toute sécurité entre threads ; Sync signifie que &T peut être partagé entre threads. Le compilateur dérive automatiquement ceux-ci pour la plupart des types, mais les pointeurs bruts ne sont pas Send/Sync par défaut. Quand vous les implémentez manuellement, vous prenez la responsabilité de la sécurité des threads. static mut nécessite un accès unsafe car plusieurs threads pourraient entrer en concurrence dessus — préférez les atomiques (AtomicU64) ou Mutex. Documentez toujours la justification de sécurité avec un commentaire SAFETY.

rust
// Some traits are unsafe to implement — the compiler can't verify invariants
use std::marker::Send;

// Send/Sync are auto-implemented, but sometimes you must manually impl
struct RawPointer<T>(*mut T);

// SAFETY: We guarantee the pointer is only used on one thread
unsafe impl<T> Send for RawPointer<T> where T: Send {}

// Splitting a slice into disjoint mutable parts
let mut v = vec![1, 2, 3, 4, 5, 6];
let (left, right) = v.split_at_mut(3);
// left = [1,2,3], right = [4,5,6] — disjoint, but compiler
// couldn't prove this without unsafe internally

// Global mutable state
static mut COUNTER: u64 = 0;
unsafe { COUNTER += 1; }  // unsafe: data races possible

Unions & assembleur en ligne

Les unions permettent à différents types de partager le même emplacement mémoire — lire un champ qui n'a pas été écrit en dernier est un comportement indéfini, d'où unsafe. Elles sont principalement pour le FFI avec C. transmute réinterprète le motif binaire d'un type vers un autre de même taille — extrêmement dangereux si les tailles diffèrent ou les types sont incompatibles. L'assembleur en ligne (asm!) vous permet d'intégrer directement des instructions CPU, utile pour le développement de noyau et l'optimisation extrême. Tous ces outils sont tranchants : utilisez-les uniquement quand aucune alternative sûre n'existe, et encapsulez-les derrière une abstraction sûre.

rust
// Unions: multiple fields share the same memory (like C unions)
#[repr(C)]
union IntOrFloat {
    i: i32,
    f: f32,
}

let mut u = IntOrFloat { i: 42 };
unsafe { println!("as int: {}", u.i); }
u.f = 3.14;
unsafe { println!("as float: {}", u.f); }
// Reading the WRONG field is undefined behavior!

// Inline assembly (nightly / asm!)
#[cfg(feature = "asm")]
unsafe fn halt() {
    std::arch::asm!("hlt", options(nostack));
}

// Transmute: reinterpret bits as another type (same size)
let bits: u32 = 0x40490FDB;  // ~3.14159 in IEEE 754
let pi: f32 = unsafe { std::mem::transmute(bits) };
16

Approfondissement des itérateurs

Trait Iterator & création

Le trait Iterator ne nécessite qu'une méthode next() qui renvoie Option<Item> — None signale l'épuisement. Tout le reste (map, filter, collect) est construit par-dessus. iter() emprunte les éléments (&T), into_iter() consomme la collection (produit des T possédés), iter_mut() produit des &mut T. Les intervalles (1..5, 1..=5) sont des itérateurs directement. Les chaînes itèrent par caractères (valeurs scalaires Unicode) ou par octets. Les itérateurs sont paresseux — rien ne s'exécute tant que vous ne les consommez pas.

rust
// The Iterator trait: one required method
trait Iterator {
    type Item;
    fn next(&mut self) -> Option<Self::Item>;
    // ... many provided methods (map, filter, etc.)
}

// Creating iterators
let v = vec![1, 2, 3];
let iter = v.iter();        // borrows: &i32
let into_iter = v.into_iter(); // owns: i32 (consumes v)
let mut_iter = v.iter_mut();   // mutably borrows: &mut i32

// Range is an iterator
for i in 1..=5 { print!("{} ", i); }  // 1 2 3 4 5

// String iteration
for c in "héllo".chars() { print!("{} ", c); }  // h é l l o
for b in "hi".bytes() { print!("{} ", b); }      // 104 105

Méthodes adaptateurs (paresseuses)

Les méthodes adaptateurs transforment les itérateurs et renvoient de nouveaux itérateurs — elles sont paresseuses, donc enchaîner map().filter().map() crée zéro collection intermédiaire. map applique une fonction à chaque élément. filter garde les éléments où le prédicat renvoie true. take(n) s'arrête après n éléments (utile pour les itérateurs infinis). skip(n) ignore les n premiers. flat_map mappe et aplatit les itérateurs imbriqués. enumerate associe chaque élément à son index. Rien ne s'exécute jusqu'à ce qu'un consommateur (collect, sum, boucle for) pilote l'itérateur.

rust
let nums = vec![1, 2, 3, 4, 5, 6];

// map: transform each element
let doubled: Vec<_> = nums.iter().map(|x| x * 2).collect();

// filter: keep elements matching predicate
let evens: Vec<_> = nums.iter().filter(|&&x| x % 2 == 0).collect();

// take / skip: limit or skip elements
let first3: Vec<_> = nums.iter().take(3).collect();     // [1,2,3]
let after2: Vec<_> = nums.iter().skip(2).collect();     // [3,4,5,6]

// flat_map: map then flatten one level
let words: Vec<_> = ["a b", "c"].iter()
    .flat_map(|s| s.split(' '))
    .collect();  // ["a","b","c"]

// enumerate: add index
for (i, v) in nums.iter().enumerate() {
    println!("{}: {}", i, v);
}

Méthodes consommatrices

Les méthodes consommatrices pilotent la chaîne d'itérateur paresseuse pour l'exécuter réellement. collect() rassemble les résultats dans n'importe quelle collection implémentant FromIterator (Vec, HashMap, String, etc.). sum/product/count/fold réduisent l'itérateur à une seule valeur. find/any/all court-circuitent — ils s'arrêtent dès que la réponse est connue, donc ils sont efficaces sur les itérateurs infinis. min/max renvoient Option (None pour les itérateurs vides). La combinaison d'adaptateurs paresseux + d'un consommateur final fait que les chaînes d'itérateurs sont aussi efficaces que des boucles écrites à la main après optimisation.

rust
let nums = vec![1, 2, 3, 4, 5];

// collect: gather into a collection
let v: Vec<i32> = nums.iter().copied().collect();
let s: std::collections::HashSet<i32> = nums.iter().copied().collect();

// sum, product, count
let total: i32 = nums.iter().sum();        // 15
let prod: i32 = nums.iter().product();     // 120
let count = nums.iter().count();           // 5

// reduce / fold: accumulate into a single value
let max = nums.iter().copied().reduce(i32::max);  // Some(5)
let sum = nums.iter().fold(0, |acc, &x| acc + x); // 15

// find / any / all: short-circuiting
let first_even = nums.iter().find(|&&x| x % 2 == 0);  // Some(2)
let has_neg = nums.iter().any(|&x| *x < 0);  // false
let all_pos = nums.iter().all(|&x| *x > 0);  // true

// min / max
nums.iter().copied().min();  // Some(1)
nums.iter().copied().max();  // Some(5)

Itérateurs personnalisés

Pour créer un itérateur personnalisé, implémentez le trait Iterator avec une méthode next(). Une fois fait, vous obtenez gratuitement toutes les 70+ méthodes adaptateurs et consommatrices. L'itérateur doit suivre son propre état (position courante, etc.) et renvoyer None quand épuisé. Implémenter IntoIterator pour votre type de collection permet la syntaxe de boucle for. Pour un accès bidirectionnel ou aléatoire, implémentez aussi DoubleEndedIterator ou ExactSizeIterator. C'est ainsi que Vec, HashMap, Range et toutes les collections standard fournissent l'itération.

rust
// Build a custom iterator for a counter
struct Counter {
    current: usize,
    max: usize,
}

impl Counter {
    fn new(max: usize) -> Self {
        Counter { current: 0, max }
    }
}

impl Iterator for Counter {
    type Item = usize;
    fn next(&mut self) -> Option<Self::Item> {
        if self.current < self.max {
            let val = self.current;
            self.current += 1;
            Some(val)
        } else {
            None
        }
    }
}

// Now all iterator methods work!
let sum: usize = Counter::new(5).sum();  // 0+1+2+3+4 = 10
let doubled: Vec<_> = Counter::new(3).map(|x| x * 10).collect();

Itérateurs infinis & chaînés

Les itérateurs Rust peuvent être infinis — (1..) génère des nombres naturels à l'infini, repeat(x) répète sans fin. Ils sont sûrs car les adaptateurs sont paresseux : take(n) limite la consommation. cycle() répète un itérateur fini à l'infini. chain() concatène des itérateurs séquentiellement. zip() apparie les éléments positionnellement (s'arrête au plus court). peekable() vous permet de regarder l'élément suivant sans le consommer — utile pour les parseurs. Le design paresseux signifie que les itérateurs infinis ne coûtent rien tant qu'ils ne sont pas consommés, et le compilateur optimise les chaînes en boucles serrées.

rust
use std::iter;

// Infinite iterators (use with take!)
let naturals = (1..).take(10);  // 1..10
let zeros = iter::repeat(0).take(5);  // [0,0,0,0,0]
let alternating = iter::repeat_with(|| rand::random::<u8>());

// cycle: repeat a finite iterator infinitely
let pattern = [1, 2, 3].iter().cycle().take(7);
// [1,2,3,1,2,3,1]

// chain: concatenate two iterators
let combined = [1, 2].iter().chain([3, 4].iter());
// [1,2,3,4]

// zip: pair elements from two iterators
let pairs: Vec<_> = [1, 2, 3].iter().zip(['a', 'b', 'c']).collect();
// [(1,'a'), (2,'b'), (3,'c')]

// peekable: look ahead without consuming
let mut iter = [1, 2, 3].iter().peekable();
if let Some(&&first) = iter.peek() { println!("{}", first); }
17

Approfondissement de la gestion d'erreurs

Combinateurs Result & Option

Les combinateurs vous permettent d'enchaîner des opérations faillibles sans expressions match imbriquées. map transforme la valeur Ok, map_err transforme l'erreur. and_then enchaîne des opérations qui renvoient elles-mêmes Result (flatmap pour les erreurs). ok_or convertit Option→Result. unwrap_or/unwrap_or_else/unwrap_or_default fournissent des valeurs de repli. Ils se composent élégamment : parse().map().and_then().map_err() crée un pipeline où chaque étape peut échouer, et le premier échec court-circuite. Préférez les combinateurs à unwrap() dans le code de production.

rust
// Result combinators chain operations without match
fn parse_and_double(s: &str) -> Result<i32, std::num::ParseIntError> {
    s.parse::<i32>().map(|n| n * 2)
}

// and_then: chain fallible operations
fn validate(n: i32) -> Result<i32, String> {
    if n > 0 { Ok(n) } else { Err("must be positive".into()) }
}
let result = "5".parse::<i32>().and_then(validate);  // Ok(5)

// map_err: transform the error type
let r = "abc".parse::<i32>()
    .map_err(|e| format!("Parse failed: {}", e));

// ok_or / ok_or_else: convert Option to Result
let opt: Option<i32> = None;
let val = opt.ok_or("missing value");  // Err("missing value")

// unwrap_or / unwrap_or_else / unwrap_or_default
let x: i32 = "abc".parse().unwrap_or(0);

Types d'erreur personnalisés

Les types d'erreur personnalisés vous permettent de représenter des échecs spécifiques au domaine. Le motif clé : implémentez From pour chaque type d'erreur sous-jacent afin que l'opérateur ? convertisse automatiquement. Cela signifie que vous pouvez utiliser ? avec std::io::Error, ParseIntError, etc. sans map_err explicite. Implémenter Display rend l'erreur conviviale ; Debug est pour les développeurs. Les erreurs basées sur des enums sont idiomatiques en Rust — elles sont exhaustives (le compilateur avertit des cas manquants) et à coût nul (pas d'allocation de tas). C'est la fondation avant d'utiliser thiserror.

rust
#[derive(Debug)]
enum AppError {
    Io(std::io::Error),
    Parse(std::num::ParseIntError),
    NotFound(String),
    Unauthorized,
}

// Implement From for automatic conversion with ?
impl From<std::io::Error> for AppError {
    fn from(e: std::io::Error) -> Self { AppError::Io(e) }
}
impl From<std::num::ParseIntError> for AppError {
    fn from(e: std::num::ParseIntError) -> Self { AppError::Parse(e) }
}

impl std::fmt::Display for AppError {
    fn fmt(&self, f: &mut std::fmt::Formatter) -> std::fmt::Result {
        match self {
            AppError::Io(e) => write!(f, "IO error: {}", e),
            AppError::Parse(e) => write!(f, "Parse error: {}", e),
            AppError::NotFound(s) => write!(f, "Not found: {}", s),
            AppError::Unauthorized => write!(f, "Unauthorized"),
        }
    }
}

// Now ? works for io::Error and ParseIntError automatically
fn read_config() -> Result<i32, AppError> {
    let s = std::fs::read_to_string("config.txt")?;  // io → AppError
    let n: i32 = s.trim().parse()?;                   // parse → AppError
    Ok(n)
}

Le trait Error & Box<dyn Error>

std::error::Error est le trait de la bibliothèque standard pour les types d'erreur (nécessite Debug + Display). Box<dyn Error> est le type d'erreur le plus simple — il accepte n'importe quelle erreur via ? et est génial pour le prototypage ou les applications où vous n'avez pas besoin de gérer des erreurs spécifiques par programme. L'inconvénient : vous perdez le type d'erreur concret, donc le match sur des variantes spécifiques nécessite downcast_ref. Pour les bibliothèques, préférez un type d'erreur enum concret (avec thiserror). Pour les applications, anyhow est un meilleur choix que Box<dyn Error> car il préserve les backtraces et les chaînes d'erreur.

rust
use std::error::Error;

// std::error::Error is the trait for all errors
// Requires: Debug + Display
fn do_something() -> Result<(), Box<dyn Error>> {
    let f = std::fs::read_to_string("file.txt")?;  // io::Error
    let n: i32 = f.parse()?;                         // ParseIntError
    println!("{}", n);
    Ok(())
}

// Box<dyn Error> is the quick-and-dirty error type
// — accepts any error via ?, but loses specific type info

// downcast to recover specific error type
match do_something() {
    Err(e) => {
        if let Some(io_err) = e.downcast_ref::<std::io::Error>() {
            println!("IO: {}", io_err);
        }
    }
    _ => {}
}

Crate thiserror (erreurs de bibliothèque)

thiserror est la crate standard pour les types d'erreur de bibliothèque. La macro #[derive(Error)] génère Display (à partir de #[error("...")]) et From (à partir de #[from]) automatiquement. #[from] fait que ? convertit l'erreur sous-jacente en votre variante d'enum. Cela élimine le boilerplate tout en gardant un enum d'erreur fortement typé et exhaustif. Utilisez thiserror pour les bibliothèques (où les appelants doivent matcher sur des erreurs spécifiques). Le placeholder {0} insère le Display de l'erreur interne ; les champs nommés comme {id} insèrent les champs de structure.

rust
use thiserror::Error;

// thiserror auto-generates Display and From impls
#[derive(Debug, Error)]
enum DataError {
    #[error("IO error: {0}")]
    Io(#[from] std::io::Error),

    #[error("parse failed: {0}")]
    Parse(#[from] std::num::ParseIntError),

    #[error("item {id} not found")]
    NotFound { id: u32 },

    #[error("invalid state: {msg}")]
    Invalid { msg: String },
}

// #[from] auto-implements From, so ? just works:
fn load() -> Result<i32, DataError> {
    let s = std::fs::read_to_string("data.txt")?;  // auto-converts
    let n: i32 = s.trim().parse()?;
    Ok(n)
}

Crate anyhow (erreurs d'application)

anyhow est la crate standard pour la gestion d'erreur des applications/binaires. anyhow::Error enveloppe n'importe quelle erreur implémentant std::error::Error et ajoute du contexte, des backtraces et des chaînes d'erreur. context() attache un message lisible par l'humain à chaque étape faillible, créant une chaîne comme « Échec de lecture de la config : erreur IO : Aucun fichier ». Cela facilite grandement le débogage — vous voyez exactement quelle étape a échoué et pourquoi. Utilisez anyhow pour main() et le code d'application où vous devez juste rapporter les erreurs, pas matcher dessus. Utilisez thiserror pour les bibliothèques où les appelants ont besoin d'erreurs typées.

rust
use anyhow::{Context, Result, anyhow};

// anyhow::Result = Result<T, anyhow::Error>
fn load_config() -> Result<String> {
    let content = std::fs::read_to_string("config.toml")
        .context("Failed to read config.toml")?;  // add context
    Ok(content)
}

fn main() -> Result<()> {
    let config = load_config()?;
    if config.is_empty() {
        // bail! / anyhow! create errors with format! syntax
        return Err(anyhow!("config is empty"));
    }
    println!("{}", config);
    Ok(())
}

// Error chain: "Failed to read config.toml: No such file..."
// anyhow preserves the full chain and backtrace
18

Approfondissement Cargo & Crates

Structure de Cargo.toml

Cargo.toml est le manifeste d'un projet Rust. [package] décrit les métadonnées de la crate. [dependencies] liste les crates externes — les chaînes de version utilisent semver (^1.0 signifie >=1.0, <2.0). features active des fonctionnalités optionnelles (la feature derive de serde active #[derive(Serialize)]). [dev-dependencies] sont seulement pour les tests/benchmarks. [features] définit des drapeaux de compilation conditionnelle. [profile.release] contrôle les paramètres d'optimisation. Le champ edition (2015/2018/2021) détermine les fonctionnalités du langage — utilisez toujours la dernière.

rust
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"
authors = ["You <[email protected]>"]
license = "MIT"
description = "A sample app"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", optional = true }
rand = "0.8"

[dev-dependencies]
criterion = "0.5"  # only for tests/benches

[features]
default = ["tokio"]
async-mode = ["dep:tokio"]

[[bin]]
name = "myapp"
path = "src/main.rs"

[profile.release]
opt-level = 3
lto = true
strip = true

Dépendances & drapeaux de features

Les drapeaux de features activent la compilation conditionnelle. Chaque dépendance peut exposer des features (par exemple, derive de serde). default-features = false supprime les features par défaut pour réduire la taille du binaire. Les dépendances optionnelles (optional = true) ne sont compilées que lorsqu'une feature les active via dep:name. Les features sont additives — elles activent des choses, ne les désactivent jamais. Cela assure l'unification des features : si deux dépendances activent différentes features de serde, Cargo compile serde une fois avec l'union de toutes les features. Utilisez #[cfg(feature = "x")] pour compiler conditionnellement du code.

rust
# Cargo.toml
[dependencies]
# Version requirements: ^1.2 (compatible), =1.2.3 (exact), >=1.0,<2.0 (range)
serde = "1.0"                    # ^1.0 (default: compatible)
serde = { version = "1.0", features = ["derive"] }
serde = { version = "1.0", default-features = false }  # disable defaults

# Optional dependencies (enabled by a feature)
tokio = { version = "1", optional = true }

[features]
# "async" feature enables the optional tokio dep
async = ["dep:tokio"]
# Features can enable other features
full = ["async", "serde/derive"]

# In code:
# #[cfg(feature = "async")]
# fn run_async() { ... }

Workspaces (projets multi-crate)

Les workspaces regroupent plusieurs crates liées qui partagent un Cargo.lock et un répertoire target. Cela accélère les builds (cache de compilation partagé) et garantit que toutes les crates utilisent les mêmes versions de dépendances. Les membres peuvent dépendre les uns des autres via path = "../core". [workspace.dependencies] centralise la gestion des versions — les crates membres les référencent avec { workspace = true }. Le resolver = "2" (par défaut dans l'édition 2021) utilise l'unification des features par cible, évitant certains problèmes de build. Utilisez les workspaces pour les monorepos, les bibliothèques à composants multiples, ou les projets séparant core/CLI/serveur.

rust
# Root Cargo.toml — workspace manifest
[workspace]
members = [
    "core",
    "cli",
    "server",
    "utils",
]
resolver = "2"

# Shared dependencies across all members
[workspace.dependencies]
serde = "1.0"
tokio = "1"

# In member crates (e.g., cli/Cargo.toml):
# [dependencies]
# serde = { workspace = true }
# core = { path = "../core" }  # local path dependency

Profils de build & optimisation

Les profils contrôlent comment cargo build votre projet. dev (par défaut pour cargo build) privilégie la vitesse de compilation. release (cargo build --release) privilégie la performance à l'exécution. Leviers clés : opt-level (0-3, 's' pour la taille, 'z' pour taille min), lto (optimisation à la liaison entre frontières de crate), codegen-units (1 = meilleure optimisation mais compilation la plus lente), strip (supprime les symboles pour des binaires plus petits), panic = 'abort' (désactive le unwinding, binaire plus petit). Pour la production, utilisez lto = true, codegen-units = 1, strip = true. Les profils personnalisés héritent des existants.

rust
# Cargo.toml
[profile.dev]
opt-level = 0        # no optimization (fast compile)
debug = true         # include debug symbols
overflow-checks = true

[profile.release]
opt-level = 3        # max optimization
lto = "fat"          # link-time optimization across crates
codegen-units = 1    # single codegen unit (better opt, slower compile)
strip = true         # strip debug symbols from binary
panic = "abort"      # smaller binary, no unwinding

[profile.release.package."*"]
opt-level = 2  # optimize dependencies less than your code

# Custom profile
[profile.bench]
inherits = "release"
debug = true  # keep symbols for profiling

# Usage: cargo build --release --profile bench

Publication & documentation

Publier sur crates.io est permanent — les versions ne peuvent pas être écrasées ou supprimées (seulement yanked, ce qui empêche les nouveaux dépendants). Assurez-vous que name, version, description, license et repository sont définis. cargo package valide le manifeste et montre ce qui serait publié. cargo doc génère la documentation HTML à partir des commentaires de doc /// — les doc tests (code dans les blocs ```) sont compilés et exécutés par cargo test. De bons commentaires de doc avec des exemples sont à la fois documentation et tests. Utilisez #[doc(hidden)] pour cacher les éléments internes.

rust
# Before publishing:
# 1. Login (one-time)
# $ cargo login <token>  (token from crates.io)

# 2. Check the package
# $ cargo package        # creates .crate file, checks metadata
# $ cargo publish --dry-run

# 3. Publish
# $ cargo publish        # uploads to crates.io (irreversible!)

# Required fields in Cargo.toml for publishing:
# name, version, description, license, repository

# Documentation:
# $ cargo doc            # generate docs for your crate + deps
# $ cargo doc --open     # generate and open in browser
# $ cargo doc --no-deps  # only your crate

# Doc tests run automatically:
/// Adds two numbers.
/// 
/// # Examples
/// ```
/// let result = mycrate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }
19

Objets de trait & dispatch dynamique

Bases de dyn Trait

Les objets de trait (dyn Trait) permettent le polymorphisme à l'exécution : une seule variable peut contenir différents types concrets implémentant le même trait. Le compilateur génère une vtable (table de méthodes virtuelles) par type, et les appels de méthodes passent par la vtable (dispatch dynamique). Cela a un petit coût à l'exécution mais permet des collections hétérogènes. Utilisez les objets de trait quand le type concret est inconnu à la compilation ou varie.

rust
trait Draw {
    fn draw(&self);
}

struct Circle { radius: f64 }
struct Square { side: f64 }

impl Draw for Circle {
    fn draw(&self) { println!("Circle r={}", self.radius); }
}
impl Draw for Square {
    fn draw(&self) { println!("Square s={}", self.side); }
}

// Trait object: type-erased, dynamic dispatch
let shapes: Vec<Box<dyn Draw>> = vec![
    Box::new(Circle { radius: 1.0 }),
    Box::new(Square { side: 2.0 }),
];
for s in &shapes { s.draw(); }

// Function taking trait object
fn render(shape: &dyn Draw) { shape.draw(); }

Sécurité objet

Un trait est object-safe (peut être utilisé comme dyn Trait) seulement si : il n'a pas de méthodes renvoyant Self, pas de méthodes prenant Self par valeur, pas de méthodes génériques, et toutes les méthodes sont dispatchables. Clone, Default et From ne sont PAS object-safe. Les solutions de contournement incluent l'utilisation de retours Box<Self> (motif box_clone), la division des traits, ou l'utilisation du dispatch statique avec des enums. Le compilateur signale clairement les violations de sécurité objet.

rust
// Object-safe trait (can be made into dyn)
trait Animal {
    fn sound(&self) -> String;
    fn name(&self) -> &str;
}

// NOT object-safe: returns Self
trait Clone {
    fn clone(&self) -> Self;  // Self is unknown for dyn
}

// NOT object-safe: takes Self by value
trait Add {
    fn add(&self, other: Self) -> Self;
}

// NOT object-safe: generic method
trait From {
    fn from<T>(t: T) -> Self;
}

// Fix: use where clauses or separate traits
trait AnimalSafe {
    fn sound(&self) -> String;
    fn box_clone(&self) -> Box<dyn AnimalSafe>;
}

Dispatch statique vs dynamique

Le dispatch statique (génériques avec bornes de trait) monomorphise : le compilateur génère une version spécialisée par type concret, permettant l'inlining et la performance maximale au coût de la taille du binaire. Le dispatch dynamique (dyn Trait) utilise des recherches de vtable à l'exécution, binaire plus petit mais appels plus lents (empêchant l'inlining). Préférez le dispatch statique pour le code critique en performance ; utilisez le dispatch dynamique pour les collections hétérogènes et les systèmes de plugins.

rust
// Static dispatch (monomorphization)
fn max<T: Ord>(a: T, b: T) -> T {
    if a > b { a } else { b }
}
// Compiler generates max_i32, max_f64, etc.

// Dynamic dispatch (vtable lookup)
fn max_dyn(a: &dyn Ord, b: &dyn Ord) -> bool {
    // a > b  // Cannot use operators on dyn
    false
}

// Trait bound (static)
fn process<T: Display>(item: &T) {
    println!("{}", item);
}

// impl Trait (static, syntactic sugar)
fn process2(item: &impl Display) {
    println!("{}", item);
}

// dyn Trait (dynamic)
fn process3(item: &dyn Display) {
    println!("{}", item);
}

Objets de trait avec durées de vie

Les objets de trait peuvent porter des bornes de durée de vie : Box<dyn Trait + 'a> signifie que l'objet de trait (et le type concret derrière) doit vivre au moins 'a. Par défaut, Box<dyn Trait> implique 'static. Quand vous stockez des objets de trait qui peuvent contenir des références, ajoutez la durée de vie explicitement. La syntaxe + combine les bornes de trait avec les durées de vie. C'est courant dans les systèmes de plugins et les gestionnaires d'événements.

rust
trait Parser {
    fn parse(&self, input: &str) -> &str;
}

// Trait object with lifetime
fn make_parser() -> Box<dyn Parser> {
    Box::new(MyParser)
}

// Trait object holding references
struct Runner<'a> {
    parsers: Vec<Box<dyn Parser + 'a>>,
}

impl<'a> Runner<'a> {
    fn add(&mut self, p: Box<dyn Parser + 'a>) {
        self.parsers.push(p);
    }
}

// dyn Trait defaults to 'static when no lifetime given
fn static_parser() -> Box<dyn Parser> {
    Box::new(MyParser)
}

Downcasting & Any

Le trait Any permet la vérification de type à l'exécution et le downcasting. Any est automatiquement implémenté pour tous les types 'static. downcast_ref et downcast_mut renvoient Option, permettant une récupération de type sûre. C'est utile pour les systèmes de plugins, les configurations dynamiques et les conteneurs hétérogènes. Utilisez Any avec parcimonie — il contourne le système de types. Préférez les enums pour les alternatives connues et les génériques pour le polymorphisme de type sûr.

rust
use std::any::Any;

// Any enables runtime type identification
let x: Box<dyn Any> = Box::new(42_i32);

// Downcast to concrete type
if let Some(n) = x.downcast_ref::<i32>() {
    println!("Got i32: {}", n);
}

// Store heterogeneous values
let mut bag: Vec<Box<dyn Any>> = vec![
    Box::new(42_i32),
    Box::new("hello".to_string()),
    Box::new(3.14_f64),
];

for item in &bag {
    if let Some(s) = item.downcast_ref::<String>() {
        println!("String: {}", s);
    } else if let Some(n) = item.downcast_ref::<i32>() {
        println!("i32: {}", n);
    }
}
20

Macros déclaratives

Bases de macro_rules!

macro_rules! définit des macros déclaratives qui correspondent à des motifs et se développent en code. $( $x:expr ),* correspond à une liste d'expressions séparées par des virgules, répétées zéro ou plusieurs fois. Le bloc $() ... * est répété pour chaque correspondance. Les macros sont développées à la compilation avant la vérification de type. Elles sont hygiéniques : les identifiants introduits par la macro n'entrent pas en collision avec le code environnant. Utilisez les macros pour réduire le boilerplate (vec!, println!, format!).

rust
macro_rules! vec_of {
    ( $( $x:expr ),* ) => {
        {
            let mut v = Vec::new();
            $(
                v.push($x);
            )*
            v
        }
    };
}

let nums = vec_of!(1, 2, 3, 4);
let strs = vec_of!("a", "b", "c");

// Multiple patterns
macro_rules! greet {
    () => { println!("Hello!") };
    ($name:expr) => { println!("Hello, {}!", $name) };
    ($name:expr, $greeting:expr) => {
        println!("{}, {}!", $greeting, $name)
    };
}
greet!();
greet!("Alice");
greet!("Bob", "Hi");

Types de fragments

Les fragments de macro ont des types spécifiques : expr (expressions), stmt (instructions), ty (types), pat (motifs), ident (identifiants), tt (arbres de tokens, le plus flexible), literal (littéraux), et plus. Le type de fragment détermine ce que la macro accepte et comment elle parse. tt est le plus général — n'importe quelle séquence de tokens valide. Utilisez le type le plus spécifique possible pour de meilleurs messages d'erreur. Le parseur suit la règle de l'ambiguïté la plus récemment ajoutée.

rust
macro_rules! items {
    // $x:expr - expression (1 + 2, foo())
    // $x:stmt - statement (let x = 5;)
    // $x:ty - type (Vec<i32>, &str)
    // $x:pat - pattern (Some(x), (a, b))
    // $x:path - path (std::vec::Vec, Module::Type)
    // $x:ident - identifier (foo, Bar)
    // $x:literal - literal (42, "hello")
    // $x:tt - token tree (anything)
    // $x:block - block ({ ... })
    // $x:item - item (fn, struct)

    ($e:expr, $t:ty, $i:ident) => {
        let $i: $t = $e;
    };
}

items!(42, i32, my_num);
items!("hi", &str, greeting);

Motifs de répétition

La répétition dans les macros : $(...)* correspond à zéro ou plusieurs, $(...)+ correspond à un ou plusieurs, $(...)? correspond à zéro ou un. Les séparateurs comme les virgules vont entre les correspondances. Les répétitions imbriquées gèrent les données multidimensionnelles (matrices, listes de listes). Le bloc d'expansion $() ... * se répète pour chaque correspondance. Les variables multiples dans la même répétition doivent correspondre le même nombre de fois. Utilisez les règles internes @prefix pour les motifs d'accumulateur.

rust
macro_rules! sum {
    // Zero or more: $(...)*
    ( $( $x:expr ),* ) => {
        {
            let mut total = 0;
            $(
                total += $x;
            )*
            total
        }
    };

    // One or more: $(...)+
    ( first $(, $x:expr )+ ) => {
        println!("First and more");
    };

    // With separator and count
    ( $( $x:expr ),+ $(; $sep:expr )? ) => {
        println!("List with optional separator");
    };
}

// Nested repetition
macro_rules! matrix {
    ( $( [ $( $x:expr ),* ] ),* ) => {
        vec![ vec![ $( $x ),* ],* ]
    };
}

Hygiène & exportation

L'hygiène des macros empêche les collisions d'identifiants : les variables introduites par une macro vivent dans leur propre portée et ne capturent pas ou ne masquent pas les variables de l'appelant. Utilisez $crate pour référencer les éléments de la crate où la macro est définie, garantissant qu'elle fonctionne après ré-exportation. La convention @prefix marque les règles d'aide internes que les utilisateurs ne devraient pas appeler directement. #[macro_export] publie la macro à la racine de la crate.

rust
// Export at crate root
#[macro_export]
macro_rules! log {
    ($($arg:tt)*) => {
        // Hygienic: this T does not collide with caller's T
        let _ts: std::time::Instant = std::time::Instant::now();
        eprintln!("[{}] {}", "LOG", format!($($arg)*));
    };
}

// Use $crate to refer to crate items (works after re-export)
#[macro_export]
macro_rules! make_error {
    ($msg:expr) => {
        $crate::Error::new($msg)
    };
}

// Internal helper rule (convention: @ prefix)
macro_rules! count {
    (@count $x:expr, $($rest:expr),*) => { 1 + count!(@count $($rest),*) };
    (@count $x:expr) => { 1 };
    () => { 0 };
}

Motifs de macro courants

Les macros excellent pour construire des DSL et réduire le boilerplate. Motifs courants : DSL de builder (html!, sql!), assertions de test (assert_approx!), configuration (config!) et génération de code (macros de type derive). Les macros sont hygiéniques et à la compilation, donc elles n'ont aucun coût à l'exécution. Limitations : pas de profondeur de récursion au-delà de 64, messages d'erreur complexes, et difficulté avec le parsing non trivial. Pour le métaprogrammation complexe, utilisez les macros procédurales.

rust
// 1. Builder DSL
macro_rules! html {
    ($tag:ident { $($body:tt)* }) => {
        format!("<{}>{}</{}>", stringify!($tag), html_inner!($($body)*), stringify!($tag))
    };
}

// 2. Test assertion
macro_rules! assert_approx {
    ($a:expr, $b:expr, $eps:expr) => {
        assert!(($a - $b).abs() < $eps, "{} != {} within {}", $a, $b, $eps);
    };
}

// 3. Configuration
macro_rules! config {
    ( $( $key:ident : $val:expr ),* ) => {
        {
            let mut m = std::collections::HashMap::new();
            $(
                m.insert(stringify!($key).to_string(), $val.to_string());
            )*
            m
        }
    };
}

let cfg = config! { host: "localhost", port: 8080 };
21

Cargo & Workspaces

Configuration du workspace

Les workspaces regroupent plusieurs crates qui partagent des dépendances et un répertoire target. Les membres sont listés explicitement ou via des globs. [workspace.package] définit les métadonnées partagées du package, héritées avec .workspace = true. [workspace.dependencies] centralise les versions de dépendances, garantissant que toutes les crates utilisent la même version. Cela évite les conflits de version et accélère les builds (un seul Cargo.lock). Utilisez les workspaces pour les projets multi-crate comme CLI + bibliothèque + serveur.

rust
# Root Cargo.toml
[workspace]
members = ["crates/*", "cli", "server"]
resolver = "2"

[workspace.package]
version = "0.1.0"
edition = "2021"
authors = ["Team <[email protected]>"]
license = "MIT"

[workspace.dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
anyhow = "1.0"

# Member crate: crates/mylib/Cargo.toml
[package]
name = "mylib"
version.workspace = true
edition.workspace = true

[dependencies]
serde.workspace = true
tokio.workspace = true

Profils de build

Les profils contrôlent les paramètres de compilation. dev privilégie la compilation rapide (opt-level 0, symboles de débogage). release maximise la performance à l'exécution (opt-level 3, LTO, une seule unité de codegen). LTO (Link-Time Optimization) permet l'inlining inter-crate. panic = "abort" produit des binaires plus petits mais désactive le unwinding. Les surcharges par package (profile.dev.package."*") optimisent les dépendances même en dev. Les profils personnalisés héritent des existants.

rust
# Cargo.toml
[profile.dev]
opt-level = 0        # No optimization (fast compile)
debug = true         # Include debug symbols
overflow-checks = true

[profile.release]
opt-level = 3        # Max optimization
debug = false
lto = "fat"          # Link-time optimization
codegen-units = 1    # Single unit (better opt, slower compile)
panic = "abort"      # Smaller binary, no unwinding
strip = true         # Strip symbols

[profile.dev.package."*"]
opt-level = 2        # Optimize dependencies in dev

# Custom profile
[profile.bench]
inherits = "release"
debug = true

# Usage: cargo build --release, cargo build --profile=bench

Features & compilation conditionnelle

Les features activent la compilation conditionnelle. Les dépendances optionnelles deviennent automatiquement des features. cfg(feature = "...") filtre le code par feature. L'ensemble de features par défaut est activé sauf si --no-default-features est passé. Les features devraient être additives (activer plus, pas moins). Utilisez les features pour réduire la taille du binaire, supporter plusieurs backends, ou filtrer le code expérimental. Combinez avec cfg_attr pour les macros derive conditionnelles. Évitez les features mutuellement exclusives.

rust
# Cargo.toml
[features]
default = ["json"]
json = ["serde_json"]
yaml = ["serde_yaml"]
async-runtime = ["tokio"]

[dependencies]
serde = { version = "1.0", optional = true }
serde_json = { version = "1.0", optional = true }
serde_yaml = { version = "0.9", optional = true }
tokio = { version = "1", optional = true, features = ["full"] }

# Code: conditional compilation
#[cfg(feature = "json")]
pub fn parse_json(s: &str) -> Result<Value, Error> {
    serde_json::from_str(s)
}

#[cfg(not(feature = "json"))]
pub fn parse_json(_: &str) -> Result<Value, Error> {
    Err(Error::FeatureNotEnabled)
}

Scripts de build (build.rs)

build.rs s'exécute avant la compilation, permettant la génération de code, l'intégration d'environnement et la liaison de bibliothèques C. Les directives cargo:rerun-if-* contrôlent quand le script se réexécute. Générez du code avec les macros println!, puis incluez!-le dans votre crate. Usages courants : intégration d'infos de version, génération de bindings (bindgen), compilation de schémas protobuf/SQL, et liaison de bibliothèques système. Gardez les scripts de build rapides — ils s'exécutent à chaque build.

rust
// build.rs: runs before compilation
use std::env;
use std::fs;
use std::path::Path;

fn main() {
    // Tell Cargo to rerun if env changes
    println!("cargo:rerun-if-env-changed=DATABASE_URL");

    let out_dir = env::var("OUT_DIR").unwrap();
    let dest = Path::new(&out_dir).join("config.rs");

    let db_url = env::var("DATABASE_URL")
        .unwrap_or_else(|_| "sqlite://default.db".to_string());

    // Generate Rust code at build time
    fs::write(&dest, format!(
        "pub const DATABASE_URL: &str = \"{}\";",
        db_url
    )).unwrap();

    // Link a C library
    println!("cargo:rustc-link-lib=static=mylib");
    println!("cargo:rustc-link-search=native=/usr/local/lib");
}

// In code: include generated file
include!(concat!(env!("OUT_DIR"), "/config.rs"));

Publication & documentation

cargo publish téléverse une crate sur crates.io. Utilisez --dry-run pour vérifier avant de publier. cargo doc génère la documentation HTML à partir des commentaires de doc (///). Les blocs de code dans les commentaires de doc sont testés avec cargo test --doc. Incluez des sections Examples, Panics et Errors. Les métadonnées (description, repository, keywords) améliorent la découvrabilité. Une fois publiée, une version ne peut pas être réutilisée ou supprimée — utilisez yank pour empêcher les nouveaux projets d'en dépendre.

rust
# Publish to crates.io
cargo login <token>     # One-time authentication
cargo publish           # Publish current crate

# Before publishing:
cargo publish --dry-run # Verify package contents
cargo package           # Inspect the .crate file

# Documentation
cargo doc               # Generate docs
cargo doc --open        # Generate and open
cargo doc --no-deps     # Only this crate

# In code: doc comments
/// Adds two numbers.
///
/// # Examples
/// ```
/// let result = mycrate::add(2, 3);
/// assert_eq!(result, 5);
/// ```
pub fn add(a: i32, b: i32) -> i32 { a + b }

# README and metadata in Cargo.toml
[package]
description = "A short description"
repository = "https://github.com/user/repo"
readme = "README.md"
keywords = ["parser", "cli"]
categories = ["command-line-utilities"]
22

Macros procédurales

Types de macros

Les macros procédurales génèrent du code Rust à la compilation, opérant sur des flux de tokens. Trois types : de type fonction (custom!()), derive (#[derive(Custom)]), et d'attribut (#[custom]). Elles nécessitent une crate séparée avec proc-macro = true. La crate syn parse la syntaxe Rust, quote génère le code, et proc-macro2 permet les tests. Les proc-macros sont puissantes mais complexes — utilisez-les pour les macros derive, les DSL et la génération de code que les macros déclaratives ne peuvent pas gérer.

rust
// Three types of procedural macros:
// 1. Function-like: my_macro!(...)
// 2. Derive: #[derive(MyMacro)]
// 3. Attribute: #[my_macro]

// Cargo.toml for a proc-macro crate
// [lib]
// proc-macro = true

// [dependencies]
// syn = { version = "2", features = ["full"] }
// quote = "1"
// proc-macro2 = "1"

use proc_macro::TokenStream;

#[proc_macro]
pub fn make_answer(_item: TokenStream) -> TokenStream {
    "fn answer() -> i32 { 42 }".parse().unwrap()
}

// Usage: make_answer!();
// Generates: fn answer() -> i32 { 42 }

Macro derive

Les macros derive ajoutent des implémentations de trait aux types annotés avec #[derive(MyMacro)]. syn parse l'entrée en un AST DeriveInput. quote! génère du code avec l'interpolation # pour les variables. Les attributs d'aide (attributes(hello)) permettent la personnalisation sur les champs ou variantes. Macros derive courantes : Debug, Clone, Serialize, Deserialize. Le code généré est ajouté au module, il ne peut donc pas modifier le type original.

rust
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, DeriveInput};

#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    let ast = parse_macro_input!(input as DeriveInput);
    let name = &ast.ident;

    let expanded = quote! {
        impl HelloMacro for #name {
            fn hello() {
                println!("Hello from {}!", stringify!(#name));
            }
        }
    };

    expanded.into()
}

// Usage:
// #[derive(HelloMacro)]
// struct Pancakes;
// Pancakes::hello();  // "Hello from Pancakes!"

// Helper attributes
#[proc_macro_derive(HelloMacro, attributes(hello))]
pub fn hello_with_attr(input: TokenStream) -> TokenStream { /* ... */ }

Macro d'attribut

Les macros d'attribut (#[my_attr]) transforment l'élément qu'elles annotent, potentiellement en le remplaçant entièrement. Elles reçoivent à la fois les arguments de l'attribut et l'élément annoté. Usages courants : logging, caching, wrappers async (#[tokio::main]), et routing (#[get("/path")]). Les macros d'attribut peuvent changer la signature de l'élément, ajouter du code, ou générer des éléments supplémentaires. Elles sont plus flexibles que les macros derive mais plus difficiles à utiliser correctement.

rust
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse_macro_input, ItemFn};

#[proc_macro_attribute]
pub fn log_calls(attr: TokenStream, item: TokenStream) -> TokenStream {
    let attr_args = syn::parse_macro_input!(attr as syn::AttributeArgs);
    let input_fn = parse_macro_input!(item as ItemFn);

    let fn_name = &input_fn.sig.ident;
    let fn_block = &input_fn.block;

    let expanded = quote! {
        fn #fn_name() {
            println!("Calling {}", stringify!(#fn_name));
            let __result = (|| #fn_block)();
            println!("Finished {}", stringify!(#fn_name));
            __result
        }
    };

    expanded.into()
}

// Usage:
// #[log_calls]
// fn my_function() -> i32 { 42 }

Macro de type fonction

Les macros procédurales de type fonction (my_macro!()) acceptent des flux de tokens arbitraires, permettant des DSL personnalisés. Implémentez Parse pour définir la syntaxe acceptée. La macro peut valider, transformer, ou générer du code basé sur l'entrée. Usages courants : requêtes SQL (sqlx), templates HTML (maud), et DSL de configuration. Contrairement aux macros déclaratives, les proc-macros peuvent parser une syntaxe complexe et effectuer des calculs arbitraires à la compilation. Gardez-les rapides pour éviter des builds lents.

rust
use proc_macro::TokenStream;
use quote::quote;
use syn::{parse::Parse, parse::ParseStream, parse_macro_input};

// Custom syntax: sql!(SELECT * FROM users WHERE id = $1)
struct SqlQuery {
    query: String,
}

impl Parse for SqlQuery {
    fn parse(input: ParseStream) -> syn::Result<Self> {
        let query = input.to_string();
        Ok(SqlQuery { query })
    }
}

#[proc_macro]
pub fn sql(input: TokenStream) -> TokenStream {
    let SqlQuery { query } = parse_macro_input!(input as SqlQuery);

    let expanded = quote! {
        {
            static QUERY: &str = #query;
            // Compile-time SQL validation could go here
            QUERY
        }
    };

    expanded.into()
}

Test & débogage

Tester les proc-macros : trybuild exécute des tests UI comparant la sortie du compilateur (succès ou messages d'erreur) à des fichiers attendus. Pour les macros derive, testez que le code généré compile et se comporte correctement. Déboguez avec eprintln! (imprimé pendant la compilation) ou cargo expand (montre la sortie de macro développée). Le développement de macros est itératif : écrivez la macro, utilisez cargo expand pour inspecter la sortie, corrigez les problèmes. Documentez clairement la syntaxe de la macro et les fonctionnalités supportées.

rust
// Cargo.toml
// [dev-dependencies]
// trybuild = "1"

// tests/ui/my_macro.rs - test file
// #[derive(MyMacro)]
// struct Foo;
// fn main() { Foo::hello(); }

// tests/ui/my_macro.stderr - expected error
// error: ...

// Test runner
#[test]
fn ui() {
    let t = trybuild::TestCases::new();
    t.pass("tests/ui/pass_*.rs");
    t.compile_fail("tests/ui/fail_*.rs");
}

// Debugging with eprintln
#[proc_macro_derive(Debug)]
pub fn debug_derive(input: TokenStream) -> TokenStream {
    eprintln!("Input tokens: {}", input);
    let ast = syn::parse2(input.clone().into()).unwrap();
    eprintln!("Parsed AST: {:#?}", ast);
    TokenStream::new()
}
23

Macros

macro_rules!

macro_rules! définit des macros déclaratives. $x est une capture, expr correspond aux expressions. $(...)* se répète. Les macros sont développées à la compilation. Utiles pour réduire le boilerplate. La macro standard vec! fonctionne similairement.

rust
macro_rules! vec_of {
    ($($x:expr),*) => {{
        let mut v = Vec::new();
        $(v.push($x);)*
        v
    }};
}
let nums = vec_of!(1, 2, 3);

Macros procédurales

Les macros procédurales génèrent du code à la compilation. Trois types : derive (#[derive(Debug)]), attribut (#[my_attr]), de type fonction (my_macro!). Plus puissantes que macro_rules! mais nécessitent une crate séparée. Utilisées par serde, tokio et diesel.

rust
// In a separate crate with proc-macro = true
use proc_macro::TokenStream;
#[proc_macro_derive(HelloMacro)]
pub fn hello_macro_derive(input: TokenStream) -> TokenStream {
    // Generate impl HelloMacro for the type
    // Returns new TokenStream
}

Macros courantes

Macros intégrées : println!/format! pour la sortie, vec! pour les vecteurs, assert!/assert_eq! pour les tests, dbg! pour le débogage, todo!/unreachable! pour le flux de contrôle. Toutes sont basées sur macro_rules!. dbg! renvoie la valeur pour le chaînage.

rust
println!("Hello, {}!", "world");
format!("x = {}", 42);
vec![1, 2, 3];
assert!(1 + 1 == 2);
assert_eq!(2 + 2, 4);
dbg!(some_variable);  // Debug print
todo!("Not implemented");
unreachable!();

Hygiène des macros

Les macros Rust sont hygiéniques : les identifiants introduits par la macro n'entrent pas en conflit avec les identifiants de la portée appelante. Cela évite des bugs subtils. Le temp à l'intérieur de la macro est différent du temp externe. Les macros déclaratives sont toujours hygiéniques.

rust
macro_rules! swap {
    ($a:expr, $b:expr) => {
        let temp = $a;
        $a = $b;
        $b = temp;
    };
}
// temp is hygienic: does not conflict with outer temp
let mut temp = 1;
let mut x = 2;
swap!(temp, x);  // Works correctly

Répétition

La répétition dans les macros : $(...)* correspond à zéro ou plusieurs, $(...)+ à un ou plusieurs. Le séparateur (virgule) peut être spécifié. $x capture chaque valeur. Utile pour des fonctions de type variadique. La macro standard println! utilise cela pour des arguments multiples.

rust
macro_rules! sum {
    ($($x:expr),*) => {
        0 $(+ $x)*
    };
}
let total = sum!(1, 2, 3, 4);  // 10
// $(...)* zero or more, $(...)+ one or more
// $(...),? optional trailing comma
24

Approfondissement d'Async

async/await

async fn renvoie un Future. .await suspend jusqu'à ce que le future soit prêt. Les Futures sont paresseux : rien ne s'exécute tant qu'ils ne sont pas attendus. Le compilateur transforme async fn en machine à états. Utilisez le runtime tokio ou async-std pour l'exécution.

rust
async fn fetch_data() -> String {
    // Simulate async work
    String::from("data")
}
async fn process() {
    let data = fetch_data().await;
    println!("{}", data);
}

Runtime Tokio

tokio::main active async main. spawn crée une tâche (comme un thread léger). join! attend plusieurs futures concurremment. Tokio fournit les I/O, minuteurs et l'ordonnancement. Le runtime async le plus populaire en Rust.

rust
#[tokio::main]
async fn main() {
    let task1 = tokio::spawn(async { work1().await });
    let task2 = tokio::spawn(async { work2().await });
    let (r1, r2) = tokio::join!(task1, task2);
}

Canaux (async)

Les canaux mpsc (multi-producteur, single-consommateur) permettent la communication async. send/recv sont async. Le canal a un tampon (32 messages). Quand tous les émetteurs sont droppés, recv renvoie None. Utile pour les motifs producteur-consommateur.

rust
use tokio::sync::mpsc;
#[tokio::main]
async fn main() {
    let (tx, mut rx) = mpsc::channel(32);
    tokio::spawn(async move {
        tx.send("hello").await.unwrap();
    });
    while let Some(msg) = rx.recv().await {
        println!("{}", msg);
    }
}

Select

select! attend le premier de plusieurs futures à se terminer. Les autres futures sont droppées. Utile pour les timeouts et les opérations en course. Le motif est courant dans les serveurs réseau. Chaque branche peut avoir un motif de garde.

rust
tokio::select! {
    result = task1 => {
        println!("Task1 done: {:?}", result);
    }
    result = task2 => {
        println!("Task2 done: {:?}", result);
    }
    _ = tokio::time::sleep(Duration::from_secs(5)) => {
        println!("Timeout");
    }
}

Stream

Stream est l'équivalent async d'Iterator. next().await obtient l'élément suivant. StreamExt fournit map, filter, for_each. Utile pour traiter des morceaux de données depuis le réseau ou des fichiers. La crate async-stream simplifie la création de streams.

rust
use tokio_stream::{self as stream, StreamExt};
let mut stream = stream::iter(vec![1, 2, 3]);
while let Some(item) = stream.next().await {
    println!("{}", item);
}
// Map, filter like iterators but async
stream.map(|x| x * 2).filter(|x| *x > 2).for_each(|x| async move {
    println!("{}", x);
}).await;
25

Cargo & Crates

Cargo.toml

Cargo.toml est le fichier manifeste. [package] définit les métadonnées. [dependencies] liste les crates externes. Les features activent les fonctionnalités optionnelles. [dev-dependencies] sont pour les tests uniquement. L'édition 2021 est la dernière stable. Les versions utilisent semver.

rust
[package]
name = "myapp"
version = "0.1.0"
edition = "2021"

[dependencies]
serde = { version = "1.0", features = ["derive"] }
tokio = { version = "1", features = ["full"] }

[dev-dependencies]
pretty_assertions = "1"

Commandes Cargo

cargo new crée un projet binaire (--lib pour une bibliothèque). build compile vers target/. --release active les optimisations. check est plus rapide que build (pas de codegen). clippy détecte les erreurs courantes. fmt formate le code. doc génère la documentation HTML. add insère une dépendance.

rust
cargo new myapp        # Create new project
cargo build            # Compile
cargo build --release  # Optimized build
cargo run              # Build and run
cargo test             # Run tests
cargo check            # Fast type-check
cargo fmt              # Format code
cargo clippy           # Lint
cargo doc --open       # Generate docs
cargo add serde        # Add dependency

Workspaces

Les workspaces regroupent plusieurs crates qui partagent un répertoire target et un Cargo.lock. Les membres sont des crates individuelles. [workspace.dependencies] centralise les versions de dépendances. Chaque crate les référence avec workspace = true. Builds plus rapides grâce à la compilation partagée. Utilisé par de grands projets comme rust-analyzer.

rust
# Root Cargo.toml
[workspace]
members = ["crate-a", "crate-b"]

# Shared dependencies
[workspace.dependencies]
serde = "1.0"

# In crate-a/Cargo.toml
[dependencies]
serde = { workspace = true }

Features

Les features activent la compilation conditionnelle. Les features par défaut sont activées sauf si --no-default-features. cfg(feature = ...) filtre le code. La syntaxe dep: dans les dépendances évite l'unification des features. Utile pour les fonctionnalités optionnelles et le code spécifique à une plateforme. Les crates peuvent exposer des features aux consommateurs.

rust
# Cargo.toml
[features]
default = ["csv"]
csv = ["dep:csv-parse"]
json = ["dep:serde_json"]

# Conditional compilation
#[cfg(feature = "csv")]
pub fn parse_csv() { /* ... */ }

Publication

crates.io est le registre de paquets Rust. cargo login authentifie. --dry-run détecte les problèmes. Une fois publiée, une version ne peut pas être republicée (yank masque seulement de la recherche). Suivez semver : patch pour les corrections, minor pour les fonctionnalités, major pour les changements cassants. README et licence sont requis.

rust
# Login (one time)
cargo login <token>

# Check before publishing
cargo publish --dry-run

# Publish to crates.io
cargo publish

# Version bumping
cargo bump patch  # 0.1.0 -> 0.1.1
cargo bump minor  # 0.1.0 -> 0.2.0
cargo bump major  # 0.1.0 -> 1.0.0
26

Tester Rust

Tests unitaires

Les tests vivent dans un module #[cfg(test)]. use super::* importe le parent. #[test] marque les fonctions de test. assert_eq! vérifie l'égalité. #[should_panic] s'attend à un panic. Les tests s'exécutent avec cargo test. Les tests unitaires sont colocalisés avec le code. L'attribut cfg(test) garantit que les tests ne sont pas compilés dans les builds release.

rust
pub fn add(a: i32, b: i32) -> i32 { a + b }

#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn test_add() {
        assert_eq!(add(2, 3), 5);
    }
    #[test]
    #[should_panic]
    fn test_panic() {
        panic!("expected");
    }
}

Tests d'intégration

Les tests d'intégration vivent dans le répertoire tests/. Chaque fichier est compilé comme une crate séparée. Ils ne peuvent tester que l'API publique. Utiles pour les tests de bout en bout. Exécutez des tests spécifiques avec --test <name>. Les tests d'intégration sont plus lents à compiler mais testent la vraie interface.

rust
// tests/integration_test.rs
use myapp::add;

#[test]
fn test_add_integration() {
    assert_eq!(add(2, 3), 5);
}
// Run: cargo test --test integration_test
// Each file in tests/ is a separate crate

Organisation des tests

#[ignore] saute un test sauf si --ignored est passé. Filtrez les tests par motif de nom. --nocapture montre la sortie println!. Les tests s'exécutent en parallèle par défaut. Utilisez --test-threads=1 pour séquentiel. Les harnesses personnalisés peuvent remplacer le runner de tests par défaut. Utile pour les benchmarks et les tests de propriété.

rust
#[test]
fn it_works() { /* ... */ }

// Custom test harness
#[test]
#[ignore = "slow test"]
fn slow_test() { /* ... */ }

// Run only ignored tests
cargo test -- --ignored

// Filter by name
cargo test test_add

// Show output
cargo test -- --nocapture

Assertions

assert! vérifie un booléen. assert_eq!/assert_ne! comparent des valeurs avec sortie de débogage en cas d'échec. Les messages personnalisés aident au débogage. Pour les nombres à virgule flottante, utilisez la crate approx. Pour l'égalité partielle, implémentez PartialEq. La sortie de débogage montre les deux valeurs quand l'assertion échoue.

rust
assert!(true);                          // Boolean
assert_eq!(2 + 2, 4);                   // Equality
assert_ne!(3, 4);                       // Inequality
assert!(x > 0, "x must be positive");   // Custom message
assert_eq!(a, b, "got {}, expected {}", a, b);
// Debug output on failure
assert_eq!(vec![1, 2], vec![1, 2]);

Tests de propriété

proptest génère des entrées aléatoires pour trouver des cas d'échec. Les stratégies (a in range) définissent les générateurs d'entrée. prop_assert! signale les échecs avec des contre-exemples minimaux. Le shrinking trouve l'entrée d'échec la plus petite. Meilleur que les tests écrits à la main pour les cas limites. Similaire à QuickCheck en Haskell.

rust
// Cargo.toml: proptest = "1"
use proptest::prelude::*;

proptest! {
    #[test]
    fn test_add_commutative(a in -1000..1000, b in -1000..1000) {
        prop_assert_eq!(add(a, b), add(b, a));
    }
    #[test]
    fn test_string_len(s in ".{0,100}") {
        prop_assert!(s.len() <= 100);
    }
}

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.