backend / rust
Programmation système avec Rust — niveau expert
Explication
Ce que vous allez apprendre
- Utiliser
BufReader/BufWriterpour réduire drastiquement le nombre d'appels système - Contrôler précisément le layout mémoire binaire avec
#[repr(C)]et l'encodage big-endian - Piloter un sous-processus depuis Rust avec
std::process::Command - Lire les arguments et variables d'environnement d'un programme
- Configurer un profil
releaseoptimisé pour un binaire système en production
Dans quel contexte ?
Une équipe infrastructure doit écrire un petit démon Rust qui lit en continu un fichier de logs volumineux, décode un protocole binaire réseau maison, et lance périodiquement un sous-processus de sauvegarde. C'est typiquement le genre de tâche pour laquelle Rust est choisi à la place du C : mêmes performances et même contrôle bas niveau, mais avec les garanties de sécurité mémoire du compilateur.
D'abord, le problème classique des I/O non bufferisées
Lire un fichier ligne par ligne sans précaution déclenche potentiellement un appel système par ligne lue, ce qui devient extrêmement coûteux sur un gros fichier. BufReader et BufWriter résolvent ce problème en regroupant les lectures et écritures dans un tampon interne, ne déclenchant un vrai appel système que lorsque ce tampon est plein (ou explicitement vidé).
Prérequis
Cette leçon s'appuie sur la gestion d'erreurs avec Result et ? ainsi que sur le memory layout (#[repr(C)], size_of) vus dans les leçons précédentes.
Une fois les I/O maîtrisées, il faut souvent parler à d'autres systèmes en binaire brut
Un protocole réseau ou un format de fichier binaire n'a pas de notion de "endianness" universelle : certains systèmes stockent les octets d'un nombre du plus significatif au moins significatif (big-endian), d'autres l'inverse (little-endian, le cas de la majorité des CPU modernes). to_be_bytes()/from_be_bytes() gèrent cette conversion explicitement, un point critique pour tout code qui communique sur le réseau.
| Outil | Rôle | Cas d'usage typique |
|---|---|---|
BufReader/BufWriter | Réduit les appels système | Traitement de gros fichiers |
#[repr(C)] | Fige le layout mémoire | Interopérabilité FFI, structures réseau |
to_be_bytes/from_be_bytes | Encodage binaire portable | Protocoles réseau, formats de fichiers |
std::process::Command | Lance un sous-processus | Orchestration d'outils externes |
Il reste un besoin courant en programmation système : dialoguer avec l'environnement et d'autres processus
std::env::args() récupère les arguments passés au programme, std::env::var() lit une variable d'environnement comme PATH, et std::process::Command lance un sous-processus et récupère sa sortie — l'équivalent Rust de fork/exec en C, mais avec une API sûre et structurée.
Piège courant
std::process::Command::output() retourne un Result : ignorer l'erreur (par exemple si l'exécutable n'existe pas sur la machine cible) fera planter silencieusement une partie de la logique. Toujours gérer explicitement le cas où le sous-processus ne peut pas être lancé, surtout en code système destiné à tourner sans supervision humaine.
Enfin, un profil de compilation pensé pour la production système
Un profil release par défaut est déjà optimisé, mais un vrai binaire système pousse souvent plus loin : lto = true active l'optimisation inter-modules (link-time optimization), codegen-units = 1 sacrifie la parallélisation de la compilation pour une meilleure optimisation finale, et panic = "abort" supprime le mécanisme de déroulement de pile pour produire un binaire plus petit et plus rapide à l'arrêt en cas de panique.
Bonne pratique
Réserve panic = "abort" aux binaires autonomes (démons, CLI) : cette option empêche catch_unwind, donc elle est à éviter dans une bibliothèque partagée où l'appelant pourrait vouloir intercepter une panique.
Ce parcours Rust touche ici à sa fin : de l'ownership aux abstractions bas niveau, tu disposes maintenant des bases nécessaires pour écrire du code système fiable, performant et sûr — la promesse originelle du langage depuis sa création chez Mozilla.
Commandes & code
Programmation système avec Rust — niveau expert
Rust rivalise avec le C/C++ pour la programmation système : pas de runtime, contrôle total de la mémoire.
use std::fs::File;
use std::io::{self, BufRead, BufReader, Write, BufWriter};
// I/O bufferise : evite un appel systeme par ligne (essentiel en systeme/perf)
fn lire_fichier_ligne_par_ligne(chemin: &str) -> io::Result<Vec<String>> {
let fichier = File::open(chemin)?;
let lecteur = BufReader::new(fichier);
let mut lignes = Vec::new();
for ligne in lecteur.lines() {
lignes.push(ligne?);
}
Ok(lignes)
}
fn ecrire_fichier_bufferise(chemin: &str, donnees: &[String]) -> io::Result<()> {
let fichier = File::create(chemin)?;
let mut ecrivain = BufWriter::new(fichier);
for ligne in donnees {
writeln!(ecrivain, "{}", ligne)?;
}
Ok(()) // BufWriter flush automatiquement au Drop, mais un flush explicite est plus sur
}
// Layout memoire precis avec #[repr] pour interoperabilite bas niveau
#[repr(C)]
struct EnTeteMessage {
version: u8,
type_message: u8,
longueur: u16, // layout identique a une struct C, exploitable en reseau/FFI
}
// Manipulation de bytes bruts : encodage/decodage binaire manuel (protocole reseau)
fn encoder_u32_big_endian(valeur: u32) -> [u8; 4] {
valeur.to_be_bytes() // big-endian : standard pour les protocoles reseau
}
fn decoder_u32_big_endian(octets: &[u8; 4]) -> u32 {
u32::from_be_bytes(*octets)
}
// Programmation sans allocation (no_std serait le vrai niveau embarque, ici on reste std
// mais on illustre l'evitement d'allocations sur un chemin critique)
fn somme_sans_allocation(donnees: &[i32]) -> i64 {
let mut total = 0i64;
for &v in donnees { // pas d'iterateur d'allocation, boucle directe sur le slice
total += v as i64;
}
total
}
fn main() -> io::Result<()> {
let donnees = vec!["ligne1".to_string(), "ligne2".to_string()];
// ecrire_fichier_bufferise("sortie.txt", &donnees)?;
// let relu = lire_fichier_ligne_par_ligne("sortie.txt")?;
let octets = encoder_u32_big_endian(1_234_567);
println!("{:?}", octets);
println!("{}", decoder_u32_big_endian(&octets));
println!("taille EnTeteMessage: {}", std::mem::size_of::<EnTeteMessage>());
let nombres = vec![1, 2, 3, 4, 5];
println!("{}", somme_sans_allocation(&nombres));
// Gestion d'arguments et de signaux systeme (base d'un vrai outil CLI/systeme)
let args: Vec<String> = std::env::args().collect();
println!("arguments: {:?}", args);
// Variables d'environnement
if let Ok(chemin) = std::env::var("PATH") {
println!("PATH configure ({} caracteres)", chemin.len());
}
// Process : lancer et attendre un sous-processus (comme exec/fork en C)
let sortie = std::process::Command::new("echo")
.arg("bonjour depuis un sous-processus")
.output();
if let Ok(sortie) = sortie {
println!("{}", String::from_utf8_lossy(&sortie.stdout));
}
Ok(())
}# Cibler des plateformes sans OS (embarque) : necessite no_std, hors du perimetre std ici
rustup target add thumbv7em-none-eabihf
# Optimisations agressives pour binaire systeme (taille et vitesse)
# Cargo.toml:
# [profile.release]
# opt-level = 3
# lto = true
# codegen-units = 1
# panic = "abort" # supprime le support de deroulement de pile, binaire plus petitRésumé
BufReader/BufWriterévitent un appel système par opération I/O — indispensable en contexte performance.#[repr(C)]etto_be_bytes/from_be_bytesdonnent un contrôle exact du layout mémoire pour FFI/réseau.std::process::Commandpilote des sous-processus, base de tout outil système en Rust.- Un profil
releaseaveclto = trueetpanic = "abort"produit des binaires système optimisés au maximum.
Exercices pratiques
Mission : un démon qui décode mal ses longueurs et ignore ses échecs
Objectif : Corriger un bug d'endianness silencieux et un sous-processus dont l'échec est ignoré, puis raisonner sur le risque de perte de logs bufferisés en cas de panic = abort.
Contexte
Un démon d'infrastructure lit un fichier binaire produit par un système tiers qui encode toutes ses longueurs en big-endian. La fonction de décodage a été écrite ainsi :
fn lire_longueur(octets: &[u8; 4]) -> u32 {
u32::from_le_bytes(*octets) // decode en little-endian
}Le démon lance aussi périodiquement un script de sauvegarde :
std::process::Command::new("backup.sh").output();Le programme ne plante jamais et tourne en apparence normalement, mais l'équipe constate en production des comportements incohérents en aval (allocations de tailles absurdes, données tronquées) qu'elle n'arrive pas à relier à une erreur précise dans les logs.