Feature di Cargo e compilazione condizionale (#[cfg]) in Rust
Pensa a quando ordini una pizza. Tutti ricevono la base e il pomodoro. Poi scegli le aggiunte: funghi, doppia mozzarella, olive. La cucina prepara solo quello che hai chiesto, così non paghi (e non aspetti) per ingredienti che non vuoi.
Il codice Rust funziona allo stesso modo. Con la compilazione condizionale (#[cfg]) dici al compilatore «compila questa parte solo su Windows» oppure «solo nei test». Con le feature di Cargo, chi usa il tuo crate sceglie le aggiunte opzionali che gli servono, come il supporto a JSON. Il codice non scelto non viene compilato affatto: non rallenta la build e non finisce nel programma finale.
Questa pagina si basa su Cargo e crate (Cargo.toml, cargo add) e su Strumenti e configurazione di Rust. Se queste parole ti sono nuove, dai prima un’occhiata lì.
#[cfg]: tenere o togliere codice
Sezione intitolata “#[cfg]: tenere o togliere codice”#[cfg(...)] è un attributo che metti su una funzione, una struct, una riga use, un modulo o anche una singola istruzione. Se la condizione è vera, il codice resta. Se è falsa, il compilatore lo toglie completamente, come se non l’avessi mai scritto.
#[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!("Ciao da {}!", os_name());}Output:
Ciao da Linux!Il Rust Playground gira su Linux, quindi stampa Ciao da Linux!. Sul tuo portatile Windows lo stesso codice stampa Ciao da Windows!. Tre funzioni hanno lo stesso nome, ma va bene così: su un dato computer ne sopravvive solo una.
C’è una piccola trappola. Su FreeBSD nessuna delle tre condizioni è vera, quindi os_name non esiste e la build si ferma con E0425: «cannot find function os_name in this scope» (funzione non trovata). Le prossime sezioni mostrano come aggiungere un caso di riserva.
cfg!(): un semplice vero o falso
Sezione intitolata “cfg!(): un semplice vero o falso”A volte non vuoi togliere codice. Vuoi solo fare una domanda. La macro cfg!() accetta le stesse condizioni, ma ti restituisce un semplice bool:
fn main() { let bits = if cfg!(target_pointer_width = "64") { 64 } else { 32 }; println!("Questo è un programma a {bits} bit"); println!("Il valore massimo di usize è {}", usize::MAX);
if cfg!(target_os = "windows") { println!("I percorsi sono tipo C:\\Users\\ana"); } else { println!("I percorsi sono tipo /home/ana"); }}Output:
Questo è un programma a 64 bitIl valore massimo di usize è 18446744073709551615I percorsi sono tipo /home/anatarget_pointer_width è la dimensione di un indirizzo di memoria: "64" su quasi tutti i computer moderni, "32" su alcuni piccoli dispositivi. È anche la dimensione di usize, ed è per questo che qui usize::MAX è così grande.
Quindi quando usare l’uno o l’altro?
#[cfg(...)] |
cfg!(...) |
|
|---|---|---|
| Cosa fa | Toglie il codice quando è falso | Diventa true o false |
| Il codice del caso «falso» compila comunque? | No, sparisce | Sì, entrambi i rami devono compilare ovunque |
| Usalo per | Codice che non può compilare altrove (API di un solo sistema, crate opzionali) | Piccole differenze di comportamento |
La riga centrale è importante. Cose solo Unix come std::os::unix::fs::PermissionsExt non esistono su Windows, quindi il codice che le usa deve stare dietro #[cfg(unix)]. Un if cfg!(unix) non basta, perché il compilatore su Windows proverebbe comunque a compilare quel ramo.
Combinare condizioni: not, any, all
Sezione intitolata “Combinare condizioni: not, any, all”Puoi costruire condizioni più grandi a partire da quelle piccole:
not(a): vera quandoaè falsaany(a, b, ...): vera quando almeno una è vera (come «oppure»)all(a, b, ...): vera quando tutte sono vere (come «e»)
Ecco di nuovo l’esempio dei sistemi operativi, questa volta con un caso di riserva, così compila ovunque:
#[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 { "sconosciuta"}
fn main() { println!("Shell predefinita: {}", shell());
if cfg!(all(unix, target_pointer_width = "64")) { println!("Un sistema Unix a 64 bit"); }}Output:
Shell predefinita: shUn sistema Unix a 64 bitunix e windows sono nomi brevi comodi. unix è vero su Linux, macOS, i BSD e sistemi simili. Queste sono le condizioni che incontrerai più spesso:
| Condizione | Vera quando… |
|---|---|
target_os = "linux" / "windows" / "macos" |
Compili per quel sistema operativo |
unix / windows |
Compili per un qualsiasi sistema tipo Unix / per Windows |
target_arch = "x86_64" / "aarch64" / "wasm32" |
Compili per quel tipo di processore (WebAssembly è wasm32) |
target_pointer_width = "64" |
Gli indirizzi (e usize) sono a 64 bit |
debug_assertions |
È una build di debug (cargo build), non --release |
test |
Stai eseguendo cargo test |
feature = "json" |
La feature di Cargo json è attiva (vedi più sotto) |
Vuoi vedere l’elenco completo per il tuo computer? Esegui rustc --print cfg. Su un PC Linux a 64 bit stampa righe come target_os="linux", target_family="unix" e debug_assertions. Vedrai spesso #[cfg(windows)] e #[cfg(unix)] nel codice FFI, perché le librerie C cambiano spesso da un sistema all’altro.
#[cfg(test)] e #[cfg_attr]
Sezione intitolata “#[cfg(test)] e #[cfg_attr]”Hai già usato una condizione senza pensarci. Ogni modulo di test della pagina sui test comincia con #[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 attiva test, quindi il modulo viene compilato e i suoi test girano. Un normale cargo build lascia fuori tutto il modulo, così il tuo programma vero non si porta dietro codice di test. #[cfg] funziona così su qualsiasi modulo (vedi Moduli), per esempio #[cfg(windows)] mod windows_paths;.
#[cfg_attr(condizione, attributo)] vuol dire: «se la condizione è vera, aggiungi questo attributo». Un uso comune è un derive che vuoi solo a volte:
#[cfg_attr(debug_assertions, derive(Debug))]struct Player { name: String, score: u32,}
fn main() { let player = Player { name: "Ana".to_string(), score: 120 };
// solo nelle build di debug; va su stderr, non su stdout #[cfg(debug_assertions)] eprintln!("debug: {:?}", player);
println!("{} ha {} punti", player.name, player.score);}Output:
Ana ha 120 puntiIn una build di debug (quella predefinita del Playground) vedi anche debug: Player { name: "Ana", score: 120 } nella parte «Standard Error» dell’output. Passa il Playground a «Release» e quella riga sparisce, insieme al codice Debug di Player. Nota che anche l’eprintln! ha bisogno del suo #[cfg(debug_assertions)]. Senza, una build di release fallisce, perché lì Player non implementa Debug. Trovi di più sulle build di debug e di release nella pagina Cargo e crate.
Le feature di Cargo nel tuo crate
Sezione intitolata “Le feature di Cargo nel tuo crate”Ora il menù della pizzeria. Una feature è un interruttore con un nome che definisci in Cargo.toml, in una tabella [features]. Costruiamo un piccolo programma, pizza_order, con due feature:
extra_cheese: un’aggiunta gratuita, attiva di default.json: stampa l’ordine in JSON. Ha bisogno dei crateserdeeserde_json, quindi vengono scaricati e compilati solo quando qualcuno chiedejson.
[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"]Riga per riga:
optional = truevuol dire «non usare questo crate a meno che una feature non lo chieda».extra_cheese = []crea una feature che non attiva nient’altro. È solo un interruttore che il tuo codice può controllare.json = ["dep:serde", "dep:serde_json"]dice: quandojsonè attiva, attiva anche queste due dipendenze opzionali.dep:vuol dire «questa è una dipendenza, non il nome di una feature».default = [...]elenca le feature attive quando nessuno dice altrimenti.
Nel codice controlli una feature con feature = "nome":
#[cfg_attr(feature = "json", derive(serde::Serialize))]struct Order { size: String, toppings: Vec<String>,}
fn main() { let mut toppings = vec!["tomato".to_string(), "basil".to_string()];
if cfg!(feature = "extra_cheese") { toppings.push("extra cheese".to_string()); }
let order = Order { size: "large".to_string(), toppings }; println!("Order: {} pizza with {}", order.size, order.toppings.join(", "));
#[cfg(feature = "json")] println!("As JSON: {}", serde_json::to_string(&order).unwrap());}Questo progetto non gira nel Playground (lì non si possono scegliere le feature di Cargo), quindi codice e output restano in inglese, esattamente come sono stati eseguiti. La riga JSON usa #[cfg], non cfg!. Senza la feature json, serde_json non è proprio una dipendenza, quindi quella riga deve sparire del tutto. Il controllo della mozzarella compila in entrambi i casi, quindi basta cfg!.
Ora ordina la tua pizza dalla riga di comando. (-q nasconde solo le righe «Compiling…» di Cargo.)
cargo run -q # default features onlycargo run -q --features json # defaults + jsoncargo run -q --no-default-features # nothing extra, not even the defaultscargo run -q --all-features # every featureNell’ordine: solo le feature predefinite; le predefinite più json; nessuna aggiunta, nemmeno le predefinite; tutte le feature. Questo è l’output reale:
$ cargo run -qOrder: large pizza with tomato, basil, extra cheese
$ cargo run -q --features jsonOrder: large pizza with tomato, basil, extra cheeseAs JSON: {"size":"large","toppings":["tomato","basil","extra cheese"]}
$ cargo run -q --no-default-featuresOrder: large pizza with tomato, basil
$ cargo run -q --all-featuresOrder: large pizza with tomato, basil, extra cheeseAs JSON: {"size":"large","toppings":["tomato","basil","extra cheese"]}Puoi combinare le opzioni. --no-default-features --features json ti dà il JSON senza la mozzarella in più. Per più feature, separale con le virgole: --features json,extra_cheese. Le stesse opzioni funzionano con cargo build, cargo check e cargo test.
Attivare le feature delle tue dipendenze
Sezione intitolata “Attivare le feature delle tue dipendenze”Il più delle volte stai dall’altra parte del bancone: usi crate che hanno delle feature. L’hai già visto nella pagina Cargo e crate:
[dependencies]serde = { version = "1", features = ["derive"] }tokio = { version = "1", features = ["full"] }serde_json = { version = "1", features = ["preserve_order"] }- La feature
derivediserdeti dà#[derive(Serialize, Deserialize)]. tokionon ha feature predefinite. Da solo non ti dà quasi niente.fullattiva tutto: il runtime,#[tokio::main], timer, rete, file e altro. Mentre impari è comodo. In un progetto vero puoi chiedere di meno, per esempiofeatures = ["rt-multi-thread", "macros"].preserve_orderdiserde_jsonmantiene le chiavi degli oggetti nell’ordine in cui le hai inserite.
cargo add scrive queste righe per te: cargo add tokio --features full.
Perché i crate si dividono in pezzi così? Perché ogni parte che non usi è codice che non devi scaricare, compilare o distribuire. Un crate grande come tokio renderebbe lenta la build di ogni piccolo progetto se compilasse sempre tutto. Le feature tengono bassi i tempi di compilazione e la dimensione dei binari.
Puoi anche spegnere le feature predefinite di un crate e scegliere solo quello che ti serve, con default-features = false:
[dependencies]serde_json = { version = "1", default-features = false, features = ["alloc"] }Per vedere quali feature sono attive nel tuo progetto, chiedilo a Cargo:
cargo tree -e features --features json --depth 1Per pizza_order stampa:
pizza_order v0.1.0 (/home/ana/pizza_order)├── serde feature "default"├── serde feature "derive"└── serde_json feature "default"Senza --depth 1 vedi l’albero intero, comprese le feature che le tue dipendenze attivano nelle loro dipendenze. È lungo, ma è lo strumento migliore quando ti chiedi «perché viene compilato questo crate?».
Regole pratiche
Sezione intitolata “Regole pratiche”Le feature dovrebbero solo aggiungere. Attivare una feature non dovrebbe mai togliere una funzione o cambiare cosa fa una funzione esistente. Ecco perché: se due crate del tuo progetto usano entrambi serde_json, e uno chiede preserve_order mentre l’altro no, Cargo compila serde_json una volta sola, con le feature di entrambi attive. Quindi ogni crate deve continuare a funzionare quando qualcun altro attiva feature in più.
Evita feature che non possono stare attive insieme. Uno schema come «feature postgres OPPURE feature sqlite, ma mai entrambe» si rompe appena due crate ne scelgono una diversa, e --all-features smette di funzionare. Preferisci feature che possono essere tutte attive contemporaneamente.
Documenta le tue feature. Scrivi un breve elenco nel README o nella documentazione del crate: il nome della feature, cosa aggiunge e se è attiva di default. Su docs.rs (in inglese) ogni crate ha una pagina «Feature flags» (per esempio quella di tokio), e molti crate segnano gli elementi con «Available on crate feature … only» (disponibile solo con la feature …).
Controlla ogni combinazione nella CI. Il codice dietro una feature viene compilato solo quando quella feature è attiva. Aggiungi questi comandi alla tua CI, così niente resta nascosto:
cargo check --all-featurescargo check --no-default-featurescargo test --all-featuresPer i crate più grandi, lo strumento cargo-hack può controllare ogni feature da sola (cargo hack check --each-feature).
Errori comuni
Sezione intitolata “Errori comuni”Un errore di battitura nel nome di una feature. Se scrivi #[cfg(feature = "jsno")], il codice compila lo stesso. Semplicemente la riga JSON non viene mai stampata, nemmeno con --features json. Per fortuna il Rust di oggi ti avvisa:
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’avviso (in inglese) dice che jsno è un valore inatteso, suggerisce il nome simile "json" ed elenca le feature che esistono davvero. Non ignorarlo. Dalla riga di comando Cargo è più severo: cargo run --features jsno si ferma con error: the package 'pizza_order' does not contain this feature: jsno e suggerisce json.
Controllare il nome della dipendenza invece di quello della feature. A causa di dep:serde, in pizza_order non esiste una feature chiamata serde. Se scrivi #[cfg_attr(feature = "serde", ...)], ricevi lo stesso avviso «unexpected cfg condition value», questa volta per serde. Poi cargo run --features json fallisce con E0277: «the trait bound Order: serde::Serialize is not satisfied», perché il derive non è mai stato aggiunto. Controlla il nome in [features]: feature = "json".
Dimenticare dep:. Quando una feature qualsiasi usa dep:serde, devi usare dep: per serde ovunque. Aggiungi una nuova feature yaml = ["serde"] e Cargo si rifiuta di leggere il file:
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 spiega che serde è una dipendenza opzionale senza una feature implicita e ti chiede di usare dep:serde. La correzione è yaml = ["dep:serde"]. E se togli dep: dappertutto, Cargo crea in silenzio una feature pubblica per ogni dipendenza opzionale (qui funzionerebbe --features serde). È uno stile vecchio. Usare dep: mantiene l’elenco delle feature pulito e voluto.
Codice che non viene mai compilato. Mettiamo che tu scriva male serde_json::to_strng dentro la riga #[cfg(feature = "json")]. cargo check dice che va tutto bene, perché di default quella riga non viene compilata. Solo cargo check --all-features lo scopre:
error[E0425]: cannot find function `to_strng` in crate `serde_json` --> src/main.rs:18:41 |18 | println!("As JSON: {}", serde_json::to_strng(&order).unwrap()); | ^^^^^^^^ |help: a function with a similar name exists |18 | println!("As JSON: {}", serde_json::to_string(&order).unwrap()); | +Il compilatore non trova la funzione to_strng e propone quella con un nome simile, to_string. È esattamente il motivo per cui --all-features va messo nella CI.
Pratica
Sezione intitolata “Pratica”1. Una cartella di configurazione per ogni sistema
Sezione intitolata “1. Una cartella di configurazione per ogni sistema”Scrivi una funzione config_dir() che restituisce dove un’app di appunti tiene le sue impostazioni: "~/.config/notes" su Linux, "~/Library/Application Support/notes" su macOS, r"%APPDATA%\notes" su Windows e "./notes" su qualsiasi altro sistema. Stampa il risultato in main. Suggerimento: una funzione per ogni caso con #[cfg(target_os = "...")], e not(any(...)) per il caso di riserva.
Mostra la soluzione
#[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!("Le impostazioni sono salvate in {}", config_dir());}Output:
Le impostazioni sono salvate in ~/.config/notesSu ogni computer sopravvive esattamente una delle quattro funzioni, quindi main trova sempre una config_dir. Il Playground gira su Linux, quindi vedi il percorso di Linux. Un’app vera userebbe un crate come dirs per trovare queste cartelle nel modo giusto.
2. Un’esportazione JSON opzionale
Sezione intitolata “2. Un’esportazione JSON opzionale”Hai un piccolo programma leaderboard che stampa dei punteggi. Aggiungi una feature export (disattivata di default) che porta dentro serde_json e aggiunge una funzione export_json. Con --features export, il programma deve stampare anche i punteggi in JSON. Suggerimento: rendi serde_json opzionale, usa dep:serde_json nella feature e metti #[cfg(feature = "export")] sia sulla funzione sia sulla riga che la chiama. Il Playground non può impostare le feature di Cargo, quindi prova questo esercizio con cargo new leaderboard sul tuo computer.
Mostra la soluzione
[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}]Non c’è nessuna riga default = [...], quindi export resta spenta a meno che tu non la chieda. Senza, serde_json non viene nemmeno scaricato. Sia la funzione sia la chiamata hanno bisogno di #[cfg], perché entrambe nominano serde_json.
Riepilogo
Sezione intitolata “Riepilogo”#[cfg(condizione)]tiene il codice solo quando la condizione è vera. Altrimenti viene tolto prima della compilazione.cfg!(condizione)è solo unbool. Entrambi i rami devono comunque compilare su ogni piattaforma.- Combina le condizioni con
not(..),any(..)eall(..). Le più comuni sonotarget_os,unix/windows,debug_assertionsetest. #[cfg_attr(condizione, attr)]aggiunge un attributo solo quando la condizione è vera.- Le feature di Cargo stanno in
[features]. Usaoptional = truepiùdep:nomeper i crate opzionali, edefault = [...]per quello che è attivo di default. - Le feature dovrebbero solo aggiungere. Esegui
cargo check --all-featuresnella CI e fai attenzione agli avvisiunexpected_cfgs.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Cosa succede a una funzione con #[cfg(target_os = "windows")] quando compili su Linux?
2. Perché if cfg!(unix) { ... } non basta per il codice che usa un'API solo Unix?
3. In Cargo.toml, cosa fa json = ["dep:serde_json"]?
4. Hai scritto per sbaglio #[cfg(feature = "jsno")]. Cosa succede?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- Features — The Cargo Book (in inglese): la guida completa a
[features], alle dipendenze opzionali, adep:e a come vengono unificate le feature. - Conditional compilation — The Rust Reference (in inglese): ogni opzione
cfg, più le regole esatte per#[cfg],#[cfg_attr]ecfg!. - La macro
cfg!— libreria standard di Rust (in inglese): cosa restituiscecfg!e in cosa differisce dall’attributo#[cfg]. - Specifying Dependencies — The Cargo Book (in inglese): come scrivere
features,default-features = falseeoptionalper ogni dipendenza. - cfg — Rust by Example (in inglese): brevi esempi di
#[cfg]ecfg!messi a confronto.