Features Cargo et compilation conditionnelle (#[cfg]) en Rust
Imaginez que vous commandez une pizza. Tout le monde a la pâte et la sauce tomate. Ensuite, vous choisissez les suppléments : champignons, fromage en plus, olives. La cuisine ne prépare que ce que vous avez demandé, donc vous ne payez pas (et n’attendez pas) pour des garnitures dont vous ne voulez pas.
Le code Rust fonctionne de la même façon. Avec la compilation conditionnelle (#[cfg]), vous dites au compilateur « ne compile cette partie que sous Windows » ou « seulement pendant les tests ». Avec les features de Cargo (fonctionnalités optionnelles), les personnes qui utilisent votre crate choisissent les suppléments dont elles ont besoin, comme la prise en charge du JSON. Le code qui n’est pas choisi n’est pas compilé du tout. Il ne ralentit pas la compilation et il n’est pas dans le programme final.
Cette page s’appuie sur Cargo et les crates (Cargo.toml, cargo add) et sur Les outils de Rust. Si ces mots sont nouveaux pour vous, jetez-y d’abord un coup d’œil rapide.
#[cfg] : garder ou retirer du code
Section intitulée « #[cfg] : garder ou retirer du code »#[cfg(...)] est un attribut que vous placez sur une fonction, une struct, une ligne use, un module ou même une seule instruction. Si la condition est vraie, le code reste. Si elle est fausse, le compilateur le retire complètement, comme si vous ne l’aviez jamais écrit.
#[cfg(target_os = "linux")]fn os_name() -> &'static str { "Linux"}
#[cfg(target_os = "windows")]fn os_name() -> &'static str { "Windows"}
#[cfg(target_os = "macos")]fn os_name() -> &'static str { "macOS"}
fn main() { println!("Bonjour depuis {} !", os_name());}Sortie :
Bonjour depuis Linux !Le Rust Playground tourne sous Linux, il affiche donc Bonjour depuis Linux !. Sur votre propre ordinateur portable sous Windows, le même code affiche Bonjour depuis Windows !. Trois fonctions portent le même nom, mais ce n’est pas un problème : sur un ordinateur donné, une seule d’entre elles survit.
Attention, il y a un petit piège. Sous FreeBSD, aucune des trois conditions n’est vraie, donc os_name n’existe pas et la compilation s’arrête avec E0425 : « cannot find function os_name in this scope » (la fonction os_name est introuvable). Les sections suivantes montrent comment ajouter une solution de repli.
cfg!() : un simple vrai ou faux
Section intitulée « cfg!() : un simple vrai ou faux »Parfois, vous ne voulez pas retirer de code. Vous voulez juste poser une question. La macro cfg!() accepte les mêmes conditions, mais elle vous renvoie un simple bool :
fn main() { let bits = if cfg!(target_pointer_width = "64") { 64 } else { 32 }; println!("Ceci est un programme {bits} bits"); println!("Le plus grand usize vaut {}", usize::MAX);
if cfg!(target_os = "windows") { println!("Les chemins ressemblent à C:\\Users\\ana"); } else { println!("Les chemins ressemblent à /home/ana"); }}Sortie :
Ceci est un programme 64 bitsLe plus grand usize vaut 18446744073709551615Les chemins ressemblent à /home/anatarget_pointer_width est la taille d’une adresse mémoire : "64" sur presque tous les ordinateurs modernes, "32" sur certains petits appareils. C’est aussi la taille de usize, ce qui explique pourquoi usize::MAX est si grand ici.
Alors, quand utiliser l’un ou l’autre ?
#[cfg(...)] |
cfg!(...) |
|
|---|---|---|
| Ce qu’il fait | Retire le code quand la condition est fausse | Devient true ou false |
| Le code « faux » est-il encore compilé ? | Non, il a disparu | Oui, les deux branches doivent compiler partout |
| À utiliser pour | Du code qui ne peut pas compiler ailleurs (API propres à un système, crates optionnelles) | De petites différences de comportement |
La ligne du milieu est importante. Les éléments propres à Unix, comme std::os::unix::fs::PermissionsExt, n’existent pas sous Windows. Le code qui les utilise doit donc se trouver derrière #[cfg(unix)]. Un if cfg!(unix) ne suffit pas, car le compilateur sous Windows essaierait quand même de compiler cette branche.
Combiner des conditions : not, any, all
Section intitulée « Combiner des conditions : not, any, all »Vous pouvez construire de grandes conditions à partir de petites :
not(a): vrai quandaest fauxany(a, b, ...): vrai quand au moins une est vraie (comme « ou »)all(a, b, ...): vrai quand toutes sont vraies (comme « et »)
Voici de nouveau l’exemple des systèmes, cette fois avec une solution de repli pour qu’il compile partout :
#[cfg(any(target_os = "linux", target_os = "macos"))]fn shell() -> &'static str { "sh"}
#[cfg(target_os = "windows")]fn shell() -> &'static str { "cmd"}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]fn shell() -> &'static str { "inconnu"}
fn main() { println!("Shell par défaut : {}", shell());
if cfg!(all(unix, target_pointer_width = "64")) { println!("Un système Unix 64 bits"); }}Sortie :
Shell par défaut : shUn système Unix 64 bitsunix et windows sont des raccourcis pratiques. unix est vrai sous Linux, macOS, les BSD et les systèmes similaires. Voici les conditions que vous rencontrerez le plus souvent :
| Condition | Vraie quand… |
|---|---|
target_os = "linux" / "windows" / "macos" |
Vous compilez pour ce système d’exploitation |
unix / windows |
Vous compilez pour n’importe quel système de type Unix / pour Windows |
target_arch = "x86_64" / "aarch64" / "wasm32" |
Vous compilez pour ce type de processeur (WebAssembly correspond à wasm32) |
target_pointer_width = "64" |
Les adresses (et usize) font 64 bits |
debug_assertions |
C’est une compilation debug (cargo build), pas --release |
test |
Vous lancez cargo test |
feature = "json" |
La feature Cargo json est activée (on y revient plus bas) |
Vous voulez voir la liste complète pour votre machine ? Lancez rustc --print cfg. Sur un PC Linux 64 bits, elle affiche des lignes comme target_os="linux", target_family="unix" et debug_assertions. Vous verrez souvent #[cfg(windows)] et #[cfg(unix)] dans le code FFI, car les bibliothèques C diffèrent souvent d’un système à l’autre.
#[cfg(test)] et #[cfg_attr]
Section intitulée « #[cfg(test)] et #[cfg_attr] »Vous avez déjà utilisé une condition sans y penser. Chaque module de tests de la page Les tests commence par #[cfg(test)] :
pub fn add_points(total: u32, points: u32) -> u32 { total + points}
#[cfg(test)]mod tests { use super::*;
#[test] fn adds_points() { assert_eq!(add_points(10, 5), 15); }}cargo test active test, donc le module est compilé et ses tests s’exécutent. Un cargo build normal laisse tout le module de côté, si bien que votre vrai programme ne contient aucun code de test. #[cfg] fonctionne de la même façon sur n’importe quel module (voir Les modules), par exemple #[cfg(windows)] mod windows_paths;.
#[cfg_attr(condition, attribut)] signifie : « si la condition est vraie, ajoute cet attribut ». Un usage courant est un derive que vous ne voulez que dans certains cas :
#[cfg_attr(debug_assertions, derive(Debug))]struct Player { name: String, score: u32,}
fn main() { let player = Player { name: "Ana".to_string(), score: 120 };
// seulement en mode debug ; va vers stderr, pas vers stdout #[cfg(debug_assertions)] eprintln!("debug: {:?}", player);
println!("{} a {} points", player.name, player.score);}Sortie :
Ana a 120 pointsEn mode debug (le mode par défaut du Playground), vous voyez aussi debug: Player { name: "Ana", score: 120 } dans la partie « Standard Error » de la sortie. Passez le Playground en « Release » et cette ligne disparaît, avec le code Debug de Player. Remarquez que le eprintln! a besoin de son propre #[cfg(debug_assertions)]. Sans lui, la compilation release échoue, car Player n’implémente pas Debug dans ce cas. Vous trouverez plus de détails sur les compilations debug et release dans Cargo et les crates.
Les features Cargo dans votre propre crate
Section intitulée « Les features Cargo dans votre propre crate »Passons au menu des pizzas. Une feature est un interrupteur nommé que vous définissez dans Cargo.toml, dans une table [features]. Construisons un petit programme, pizza_order, avec deux features :
extra_cheese: un supplément gratuit, activé par défaut.json: affiche la commande en JSON. Elle a besoin des cratesserdeetserde_json, qui ne sont donc téléchargées et compilées que si quelqu’un demandejson.
[package]name = "pizza_order"version = "0.1.0"edition = "2024"
[dependencies]serde = { version = "1", features = ["derive"], optional = true }serde_json = { version = "1", optional = true }
[features]default = ["extra_cheese"]extra_cheese = []json = ["dep:serde", "dep:serde_json"]Ligne par ligne :
optional = truesignifie « n’utilise pas cette crate, sauf si une feature la demande ».extra_cheese = []crée une feature qui n’active rien d’autre. C’est juste un interrupteur que votre code peut consulter.json = ["dep:serde", "dep:serde_json"]dit : quandjsonest activée, active aussi ces deux dépendances optionnelles.dep:signifie « ceci est une dépendance, pas un nom de feature ».default = [...]liste les features activées quand personne ne précise rien.
Dans le code, vous testez une feature avec feature = "nom" :
#[cfg_attr(feature = "json", derive(serde::Serialize))]struct Order { size: String, toppings: Vec<String>,}
fn main() { let mut toppings = vec!["tomate".to_string(), "basilic".to_string()];
if cfg!(feature = "extra_cheese") { toppings.push("fromage en plus".to_string()); }
let order = Order { size: "grande".to_string(), toppings }; println!("Commande : une {} pizza avec {}", order.size, order.toppings.join(", "));
#[cfg(feature = "json")] println!("En JSON : {}", serde_json::to_string(&order).unwrap());}La ligne JSON utilise #[cfg], pas cfg!. Sans la feature json, serde_json n’est même pas une dépendance, donc cette ligne doit disparaître complètement. Le test du fromage compile sans problème dans tous les cas, donc cfg! suffit.
Maintenant, commandez votre pizza depuis la ligne de commande. (-q masque simplement les lignes « Compiling… » de Cargo.)
cargo run -q # features par défaut uniquementcargo run -q --features json # par défaut + jsoncargo run -q --no-default-features # rien en plus, pas même celles par défautcargo run -q --all-features # toutes les featuresVoici la vraie sortie :
$ cargo run -qCommande : une grande pizza avec tomate, basilic, fromage en plus
$ cargo run -q --features jsonCommande : une grande pizza avec tomate, basilic, fromage en plusEn JSON : {"size":"grande","toppings":["tomate","basilic","fromage en plus"]}
$ cargo run -q --no-default-featuresCommande : une grande pizza avec tomate, basilic
$ cargo run -q --all-featuresCommande : une grande pizza avec tomate, basilic, fromage en plusEn JSON : {"size":"grande","toppings":["tomate","basilic","fromage en plus"]}Vous pouvez combiner les options. --no-default-features --features json donne le JSON sans le fromage. Pour plusieurs features, séparez-les par des virgules : --features json,extra_cheese. Les mêmes options fonctionnent avec cargo build, cargo check et cargo test.
Activer les features de vos dépendances
Section intitulée « Activer les features de vos dépendances »La plupart du temps, vous êtes de l’autre côté du comptoir : vous utilisez des crates qui ont des features. Vous l’avez déjà vu dans Cargo et les crates :
[dependencies]serde = { version = "1", features = ["derive"] }tokio = { version = "1", features = ["full"] }serde_json = { version = "1", features = ["preserve_order"] }- La feature
derivedeserdevous donne#[derive(Serialize, Deserialize)]. tokion’a aucune feature par défaut. Seule, elle ne vous donne presque rien.fullactive tout : le runtime,#[tokio::main], les minuteurs, le réseau, les fichiers et plus encore. C’est pratique pendant l’apprentissage. Dans un vrai projet, vous pouvez en demander moins, par exemplefeatures = ["rt-multi-thread", "macros"].preserve_orderdeserde_jsongarde les clés des objets dans l’ordre où vous les avez insérées.
cargo add écrit ces lignes pour vous : cargo add tokio --features full.
Pourquoi les crates sont-elles découpées ainsi ? Parce que chaque partie que vous n’utilisez pas, c’est du code que vous n’avez pas à télécharger, à compiler ni à livrer. Une grosse crate comme tokio rendrait la compilation de chaque petit projet très lente si elle compilait toujours tout. Les features réduisent les temps de compilation et la taille des binaires.
Vous pouvez aussi désactiver les features par défaut d’une crate et ne choisir que ce dont vous avez besoin, avec default-features = false :
[dependencies]serde_json = { version = "1", default-features = false, features = ["alloc"] }Pour voir quelles features sont activées dans votre projet, demandez à Cargo :
cargo tree -e features --features json --depth 1Pour pizza_order, cela affiche :
pizza_order v0.1.0 (/home/ana/pizza_order)├── serde feature "default"├── serde feature "derive"└── serde_json feature "default"Sans --depth 1, vous voyez l’arbre complet, y compris les features que vos dépendances activent dans leurs propres dépendances. C’est long, mais c’est le meilleur outil quand vous vous demandez « pourquoi cette crate est-elle compilée ? ».
Règles pratiques
Section intitulée « Règles pratiques »Les features ne devraient qu’ajouter des choses. Activer une feature ne devrait jamais retirer une fonction ni changer ce que fait une fonction existante. Voici pourquoi. Si deux crates de votre projet utilisent toutes les deux serde_json, et que l’une demande preserve_order alors que l’autre non, Cargo compile serde_json une seule fois, avec les features des deux activées. Chaque crate doit donc continuer à fonctionner quand quelqu’un d’autre active des features en plus.
Évitez les features qui ne peuvent pas être activées ensemble. Une conception du type « feature postgres OU feature sqlite, mais jamais les deux » casse dès que deux crates en choisissent une différente, et --all-features ne fonctionne plus. Préférez des features qui peuvent toutes être activées en même temps.
Documentez vos features. Écrivez une courte liste dans votre README ou dans la documentation de votre crate : le nom de la feature, ce qu’elle ajoute et si elle est activée par défaut. Sur docs.rs, chaque crate a une page « Feature flags » (par exemple celle de tokio), et beaucoup de crates marquent certains éléments avec « Available on crate feature … only » (disponible seulement avec la feature …).
Vérifiez toutes les combinaisons en CI. Le code derrière une feature n’est compilé que si cette feature est activée. Ajoutez ces commandes à votre CI pour que rien ne s’y cache :
cargo check --all-featurescargo check --no-default-featurescargo test --all-featuresPour les crates plus grosses, l’outil cargo-hack peut vérifier chaque feature séparément (cargo hack check --each-feature).
Erreurs courantes
Section intitulée « Erreurs courantes »Une faute de frappe dans un nom de feature. Si vous écrivez #[cfg(feature = "jsno")], le code compile quand même. La ligne JSON ne s’affiche simplement jamais, même avec --features json. Heureusement, les versions actuelles de Rust vous préviennent :
warning: unexpected `cfg` condition value: `jsno` --> src/main.rs:17:11 |17 | #[cfg(feature = "jsno")] | ^^^^^^^^^^------ | | | help: there is a expected value with a similar name: `"json"` | = note: expected values for `feature` are: `default`, `extra_cheese`, and `json` = help: consider adding `jsno` as a feature in `Cargo.toml` = note: `#[warn(unexpected_cfgs)]` on by defaultL’avertissement dit que la valeur jsno est inattendue, qu’il en existe une au nom proche ("json"), et quelles features existent vraiment. Ne l’ignorez pas. En ligne de commande, Cargo est plus strict : cargo run --features jsno s’arrête avec error: the package 'pizza_order' does not contain this feature: jsno (le paquet n’a pas cette feature) et vous suggère json.
Tester le nom de la dépendance au lieu du nom de la feature. À cause de dep:serde, il n’existe aucune feature appelée serde dans pizza_order. Si vous écrivez #[cfg_attr(feature = "serde", ...)], vous obtenez le même avertissement « unexpected cfg condition value », cette fois pour serde. Ensuite, cargo run --features json échoue avec E0277 : « the trait bound Order: serde::Serialize is not satisfied » (Order n’implémente pas serde::Serialize), parce que le derive n’a jamais été ajouté. Utilisez le nom qui figure dans [features] : feature = "json".
Oublier dep:. Dès qu’une feature écrit dep:serde, vous devez utiliser dep: pour serde partout. Ajoutez une nouvelle feature yaml = ["serde"] et Cargo refuse de lire le fichier :
error: failed to parse manifest at `/home/ana/pizza_order/Cargo.toml`
Caused by: feature `yaml` includes `serde`, but `serde` is an optional dependency without an implicit feature Use `dep:serde` to enable the dependency.Cargo vous explique que serde est une dépendance optionnelle sans feature implicite, et vous demande d’utiliser dep:serde. La correction est yaml = ["dep:serde"]. Et si vous n’écrivez dep: nulle part, Cargo crée discrètement une feature publique pour chaque dépendance optionnelle (ici, --features serde fonctionnerait). C’est un style ancien. Utiliser dep: garde votre liste de features claire et explicite.
Du code qui n’est jamais compilé. Imaginons que vous tapiez serde_json::to_strng par erreur dans la ligne #[cfg(feature = "json")]. cargo check dit que tout va bien, car cette ligne n’est pas compilée par défaut. Seul cargo check --all-features trouve l’erreur :
error[E0425]: cannot find function `to_strng` in crate `serde_json` --> src/main.rs:18:42 |18 | println!("En JSON : {}", serde_json::to_strng(&order).unwrap()); | ^^^^^^^^ |help: a function with a similar name exists |18 | println!("En JSON : {}", serde_json::to_string(&order).unwrap()); | +Le compilateur ne trouve pas to_strng dans serde_json et vous propose to_string. C’est exactement pour cette raison que --all-features a sa place dans votre CI.
Exercices
Section intitulée « Exercices »1. Un dossier de configuration pour chaque système
Section intitulée « 1. Un dossier de configuration pour chaque système »Écrivez une fonction config_dir() qui renvoie l’endroit où une application de notes range ses réglages : "~/.config/notes" sous Linux, "~/Library/Application Support/notes" sous macOS, r"%APPDATA%\notes" sous Windows, et "./notes" sur tous les autres systèmes. Affichez le résultat dans main. Indice : une fonction par cas avec #[cfg(target_os = "...")], et not(any(...)) pour la solution de repli.
Voir la solution
#[cfg(target_os = "linux")]fn config_dir() -> &'static str { "~/.config/notes"}
#[cfg(target_os = "macos")]fn config_dir() -> &'static str { "~/Library/Application Support/notes"}
#[cfg(target_os = "windows")]fn config_dir() -> &'static str { r"%APPDATA%\notes"}
#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))]fn config_dir() -> &'static str { "./notes"}
fn main() { println!("Les réglages sont enregistrés dans {}", config_dir());}Sortie :
Les réglages sont enregistrés dans ~/.config/notesSur n’importe quel ordinateur, exactement une des quatre fonctions survit, donc main trouve toujours un config_dir. Le Playground tourne sous Linux, vous voyez donc le chemin Linux. Une vraie application utiliserait une crate comme dirs pour trouver ces dossiers correctement.
2. Un export JSON optionnel
Section intitulée « 2. Un export JSON optionnel »Vous avez un petit programme leaderboard qui affiche des scores. Ajoutez une feature export (désactivée par défaut) qui fait venir serde_json et ajoute une fonction export_json. Avec --features export, le programme doit aussi afficher les scores en JSON. Indice : rendez serde_json optionnelle, utilisez dep:serde_json dans la feature, et mettez #[cfg(feature = "export")] à la fois sur la fonction et sur la ligne qui l’appelle. Le Playground ne permet pas de choisir les features Cargo, alors essayez celui-ci sur votre machine avec cargo new leaderboard.
Voir la solution
[package]name = "leaderboard"version = "0.1.0"edition = "2024"
[dependencies]serde_json = { version = "1", optional = true }
[features]export = ["dep:serde_json"]struct Score { name: &'static str, points: u32,}
#[cfg(feature = "export")]fn export_json(scores: &[Score]) -> String { let list: Vec<serde_json::Value> = scores .iter() .map(|s| serde_json::json!({ "name": s.name, "points": s.points })) .collect(); serde_json::Value::Array(list).to_string()}
fn main() { let scores = [ Score { name: "Ana", points: 120 }, Score { name: "Bo", points: 95 }, ];
for s in &scores { println!("{} : {} points", s.name, s.points); }
#[cfg(feature = "export")] println!("Export : {}", export_json(&scores));}$ cargo run -qAna : 120 pointsBo : 95 points
$ cargo run -q --features exportAna : 120 pointsBo : 95 pointsExport : [{"name":"Ana","points":120},{"name":"Bo","points":95}]Il n’y a pas de ligne default = [...], donc export est désactivée tant que vous ne la demandez pas. Sans elle, serde_json n’est même pas téléchargée. La fonction et l’appel ont tous les deux besoin de #[cfg], car chacun d’eux mentionne serde_json.
Récapitulatif
Section intitulée « Récapitulatif »#[cfg(condition)]ne garde le code que si la condition est vraie. Sinon, il est retiré avant la compilation.cfg!(condition)n’est qu’unbool. Les deux branches doivent quand même compiler sur toutes les plateformes.- Combinez des conditions avec
not(..),any(..)etall(..). Les plus courantes sonttarget_os,unix/windows,debug_assertionsettest. #[cfg_attr(condition, attribut)]ajoute un attribut seulement si la condition est vraie.- Les features Cargo se trouvent dans
[features]. Utilisezoptional = trueavecdep:nompour les crates optionnelles, etdefault = [...]pour ce qui est activé par défaut. - Les features ne devraient qu’ajouter des choses. Lancez
cargo check --all-featuresen CI et surveillez les avertissementsunexpected_cfgs.
Petit quiz
Section intitulée « Petit quiz »Petit quiz
1. Que devient une fonction marquée #[cfg(target_os = "windows")] quand vous compilez sous Linux ?
2. Pourquoi if cfg!(unix) { ... } ne suffit-il pas pour du code qui utilise une API propre à Unix ?
3. Dans Cargo.toml, que fait json = ["dep:serde_json"] ?
4. Vous avez écrit #[cfg(feature = "jsno")] par erreur. Que se passe-t-il ?
Score : 0 / 4
Pour aller plus loin
Section intitulée « Pour aller plus loin »- Features — The Cargo Book (en anglais) : le guide complet de
[features], des dépendances optionnelles, dedep:et de l’unification des features. - Conditional compilation — The Rust Reference (en anglais) : toutes les options de
cfg, et les règles exactes de#[cfg],#[cfg_attr]etcfg!. - Macro
cfg!— bibliothèque standard de Rust (en anglais) : ce que renvoiecfg!et en quoi il diffère de l’attribut#[cfg]. - Specifying Dependencies — The Cargo Book (en anglais) : comment écrire
features,default-features = falseetoptionalpour chaque dépendance. - cfg — Rust by Example (en anglais) : de courts exemples de
#[cfg]etcfg!côte à côte.