Frameworks et écosystème Rust¶
L'écosystème Rust pour le développement web et système s'est consolide autour de quelques crates incontournables. Contrairement a d'autres langages, Rust n'a pas de framework "officiel" : la communauté a converge vers des solutions complementaires qui composent bien entre elles, notamment grâce au runtime async Tokio et au système de traits de la stdlib.
Comparatif des frameworks web¶
| Framework | Approche | Async runtime | Points forts | Cas d'usage typique |
|---|---|---|---|---|
| Actix Web | Actor model | Tokio | Très haute performance, mature, vaste API | APIs haute fréquence, microservices |
| Axum | Tower/Hyper | Tokio | Ergonomique, extracteurs puissants, idiomatique | APIs REST modernes, intégration Tokio |
| Rocket | Macros | Tokio | API intuitive, guards, formes | Prototypage rapide, petits services |
| Warp | Filtres composables | Tokio | Très modulaire, testable unitairement | APIs avec logique de routage complexe |
Axum — framework web idiomatique¶
Axum est développé par l'équipe Tokio et est considéré comme le framework le plus idiomatique aujourd'hui. Il s'appuie directement sur Tower pour le middleware et Hyper pour le transport HTTP.
// Cargo.toml
// [dependencies]
// axum = "0.7"
// tokio = { version = "1", features = ["full"] }
// serde = { version = "1", features = ["derive"] }
// serde_json = "1"
use axum::{
extract::{Path, State},
http::StatusCode,
response::Json,
routing::{get, post},
Router,
};
use serde::{Deserialize, Serialize};
use std::sync::Arc;
use tokio::sync::RwLock;
#[derive(Clone, Serialize, Deserialize)]
struct Item {
id: u32,
nom: String,
}
// Etat partage entre les handlers via Arc<RwLock<_>>
type SharedState = Arc<RwLock<Vec<Item>>>;
// Handler GET /items — retourne la liste
async fn lister_items(State(state): State<SharedState>) -> Json<Vec<Item>> {
let items = state.read().await;
Json(items.clone())
}
// Handler GET /items/:id — retourne un item ou 404
async fn get_item(
Path(id): Path<u32>,
State(state): State<SharedState>,
) -> Result<Json<Item>, StatusCode> {
let items = state.read().await;
items
.iter()
.find(|i| i.id == id)
.cloned()
.map(Json)
.ok_or(StatusCode::NOT_FOUND)
}
#[tokio::main]
async fn main() {
let state: SharedState = Arc::new(RwLock::new(vec![
Item { id: 1, nom: "Premier item".into() },
]));
let app = Router::new()
.route("/items", get(lister_items))
.route("/items/:id", get(get_item))
.with_state(state);
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Actix Web — performance maximale¶
Actix Web est l'un des frameworks web les plus rapides tous langages confondus selon les benchmarks TechEmpower. Son modèle est base sur des acteurs Actix, mais son API publique ressemble a celle d'Axum.
// Cargo.toml
// [dependencies]
// actix-web = "4"
// serde = { version = "1", features = ["derive"] }
use actix_web::{get, post, web, App, HttpResponse, HttpServer, Responder};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
struct Payload {
message: String,
}
// Macro #[get] — associe la fonction a la route GET /ping
#[get("/ping")]
async fn ping() -> impl Responder {
HttpResponse::Ok().json(Payload {
message: "pong".into(),
})
}
// Handler avec extraction du corps JSON
#[post("/echo")]
async fn echo(body: web::Json<Payload>) -> impl Responder {
HttpResponse::Ok().json(body.into_inner())
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| {
App::new()
.service(ping)
.service(echo)
})
.bind("0.0.0.0:8080")?
.run()
.await
}
Tokio — runtime async de référence¶
Tokio est le runtime asynchrone le plus utilisé dans l'écosystème Rust. Il fournit un scheduler multi-thread, des I/O non bloquantes, des timers et des primitives de synchronisation.
// Cargo.toml
// [dependencies]
// tokio = { version = "1", features = ["full"] }
use tokio::{
fs::File,
io::{AsyncReadExt, AsyncWriteExt},
time::{sleep, Duration},
};
// async fn — fonction asynchrone Tokio
// Les points d'attente (.await) cedent le controle au scheduler
async fn lire_fichier(chemin: &str) -> Result<String, std::io::Error> {
let mut fichier = File::open(chemin).await?; // I/O non bloquante
let mut contenu = String::new();
fichier.read_to_string(&mut contenu).await?;
Ok(contenu)
}
async fn tache_periodique() {
loop {
println!("Tick");
sleep(Duration::from_secs(1)).await; // Attend sans bloquer le thread
}
}
#[tokio::main]
async fn main() {
// Lancer plusieurs taches concurrentes avec tokio::spawn
let handle = tokio::spawn(tache_periodique());
// tokio::join! execute des futures en parallele
let (r1, r2) = tokio::join!(
lire_fichier("/etc/hostname"),
lire_fichier("/etc/os-release"),
);
println!("hostname : {:?}", r1);
handle.abort(); // Arret de la tache periodique
}
Tokio vs async-std
Tokio est le choix standard pour les projets en production. async-std propose une API plus proche de la stdlib mais est moins activement maintenu. La majorité des crates de l'écosystème (Axum, sqlx, reqwest) ciblent Tokio en priorité.
Rocket — ergonomie et guards¶
Rocket est apprecie pour la clarte de son API. Ses "request guards" permettent d'injecter des valeurs validees directement dans les parametres des handlers.
// Cargo.toml
// [dependencies]
// rocket = { version = "0.5", features = ["json"] }
// serde = { version = "1", features = ["derive"] }
#[macro_use]
extern crate rocket;
use rocket::serde::{json::Json, Deserialize, Serialize};
#[derive(Serialize, Deserialize)]
#[serde(crate = "rocket::serde")]
struct Reponse {
statut: String,
valeur: u32,
}
// #[get] et les parametres de route sont associes par nom
#[get("/calcul/<a>/<b>")]
fn additionner(a: u32, b: u32) -> Json<Reponse> {
Json(Reponse {
statut: "ok".into(),
valeur: a + b,
})
}
#[launch]
fn rocket() -> _ {
rocket::build().mount("/api", routes![additionner])
}
CLI — Clap¶
Clap est la référence pour le parsing des arguments en ligne de commande. Son API dérivé permet de définir la CLI directement à partir d'une struct.
// Cargo.toml
// [dependencies]
// clap = { version = "4", features = ["derive"] }
use clap::{Parser, Subcommand};
/// Outil de gestion de fichiers — exemple Clap 4
#[derive(Parser)]
#[command(name = "monoutil", version, about)]
struct Cli {
/// Niveau de verbosity (-v, -vv, -vvv)
#[arg(short, long, action = clap::ArgAction::Count)]
verbeux: u8,
#[command(subcommand)]
commande: Commande,
}
#[derive(Subcommand)]
enum Commande {
/// Lister les fichiers dans un repertoire
Lister {
/// Chemin du repertoire (defaut : .)
#[arg(default_value = ".")]
chemin: String,
},
/// Compter les lignes d'un fichier
Compter {
/// Fichier a analyser
fichier: String,
},
}
fn main() {
let cli = Cli::parse();
match cli.commande {
Commande::Lister { chemin } => {
println!("Listage de : {}", chemin);
}
Commande::Compter { fichier } => {
println!("Comptage dans : {}", fichier);
}
}
}
Écosystème complementaire¶
| Crate | Catégorie | Rôle |
|---|---|---|
| serde | Serialisation | Framework de serialisation/deserialisation universel |
| serde_json | Serialisation | Support JSON pour serde |
| sqlx | Base de données | Requêtes SQL asynchrones avec vérification à la compilation |
| diesel | Base de données | ORM synchrone avec DSL de requêtes type-safe |
| reqwest | HTTP client | Client HTTP asynchrone base sur Tokio + Hyper |
| tower | Middleware | Abstraction Service/Layer pour les middlewares |
| tracing | Observabilité | Framework de traces et logs structures |
| anyhow | Gestion d'erreurs | Boite d'erreurs flexible pour les applications |
| thiserror | Gestion d'erreurs | Macros dérivé pour les types d'erreurs de bibliotheques |
| uuid | Identifiants | Génération et parsing d'UUIDs |
| chrono | Date/heure | Types date/heure avec fuseaux horaires |
| rayon | Parallélisme | Iterateurs parallèles data-parallelism |
| regex | Expressions reg. | Moteur de regex performant (NFA, pas de backtracking) |
| wasm-bindgen | WebAssembly | Bindings entre Rust/WASM et JavaScript |
Serde et les dérivés
Serde utilise des macros procedurales (#[derive(Serialize, Deserialize)]) qui augmentent le temps de compilation. Dans les grands projets, il est recommande d'activer le cache incremental de Cargo et d'envisager serde avec default-features = false pour les cibles WASM ou embarquées.