Features do Cargo e compilação condicional (#[cfg]) em Rust
Pense em pedir uma pizza. Toda pizza vem com a massa e o molho de tomate. Depois você escolhe os adicionais: champignon, queijo extra, azeitona. A cozinha só prepara o que você pediu, então você não paga (nem espera) por ingredientes que não quer.
O código Rust funciona do mesmo jeito. Com a compilação condicional (#[cfg]), você diz ao compilador “compile esta parte só no Windows” ou “só nos testes”. Com as features do Cargo (recursos opcionais), quem usa o seu crate escolhe os adicionais de que precisa, como suporte a JSON. O código que ninguém escolheu simplesmente não é compilado: não deixa o build mais lento e não vai parar no programa final.
Esta página parte do que você viu em Cargo e crates (Cargo.toml, cargo add) e Ferramentas do Rust. Se essas palavras ainda são novidade, dê uma olhada rápida lá primeiro.
#[cfg]: manter ou remover código
Seção intitulada “#[cfg]: manter ou remover código”#[cfg(...)] é um atributo que você coloca em uma função, um struct, uma linha use, um módulo ou até em uma única instrução. Se a condição for verdadeira, o código fica. Se for falsa, o compilador remove o código por completo, como se você nunca o tivesse escrito.
#[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!("Olá do {}!", os_name());}Saída:
Olá do Linux!O Rust Playground roda em Linux, por isso imprime Olá do Linux!. No seu notebook com Windows, o mesmo código imprime Olá do Windows!. São três funções com o mesmo nome, mas tudo bem: em cada computador, só uma delas sobrevive.
Só que tem uma pegadinha. No FreeBSD, nenhuma das três condições é verdadeira, então os_name não existe e o build para com o E0425: “cannot find function os_name in this scope” (a função os_name não foi encontrada neste escopo). Nas próximas seções você vai ver como adicionar uma alternativa.
cfg!(): um simples verdadeiro ou falso
Seção intitulada “cfg!(): um simples verdadeiro ou falso”Às vezes você não quer remover código. Você só quer fazer uma pergunta. A macro cfg!() aceita as mesmas condições, mas devolve um simples bool:
fn main() { let bits = if cfg!(target_pointer_width = "64") { 64 } else { 32 }; println!("Este é um programa de {bits} bits"); println!("O maior usize é {}", usize::MAX);
if cfg!(target_os = "windows") { println!("Os caminhos são assim: C:\\Users\\ana"); } else { println!("Os caminhos são assim: /home/ana"); }}Saída:
Este é um programa de 64 bitsO maior usize é 18446744073709551615Os caminhos são assim: /home/anatarget_pointer_width é o tamanho de um endereço de memória: "64" em quase todo computador moderno, "32" em alguns dispositivos pequenos. Também é o tamanho de usize, e é por isso que usize::MAX é tão grande aqui.
Então, quando usar cada um?
#[cfg(...)] |
cfg!(...) |
|
|---|---|---|
| O que faz | Remove o código quando é falso | Vira true ou false |
| O código “falso” ainda é compilado? | Não, ele some | Sim, os dois ramos precisam compilar em todo lugar |
| Use para | Código que não consegue compilar em outros lugares (APIs de um só sistema, crates opcionais) | Pequenas diferenças de comportamento |
A linha do meio é a que importa. Coisas que só existem no Unix, como std::os::unix::fs::PermissionsExt, não existem no Windows, então o código que as usa precisa ficar atrás de um #[cfg(unix)]. Um if cfg!(unix) não basta, porque o compilador no Windows ainda tentaria compilar aquele ramo.
Combinando condições: not, any, all
Seção intitulada “Combinando condições: not, any, all”Você pode montar condições maiores a partir de condições pequenas:
not(a): verdadeira quandoaé falsaany(a, b, ...): verdadeira quando pelo menos uma é verdadeira (como “ou”)all(a, b, ...): verdadeira quando todas são verdadeiras (como “e”)
Aqui está o exemplo dos sistemas operacionais de novo, agora com uma alternativa para compilar em qualquer lugar:
#[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 { "desconhecido"}
fn main() { println!("Shell padrão: {}", shell());
if cfg!(all(unix, target_pointer_width = "64")) { println!("Um sistema Unix de 64 bits"); }}Saída:
Shell padrão: shUm sistema Unix de 64 bitsunix e windows são apelidos curtos e práticos. unix é verdadeiro no Linux, no macOS, nos BSDs e em sistemas parecidos. Estas são as condições que você mais vai encontrar:
| Condição | Verdadeira quando… |
|---|---|
target_os = "linux" / "windows" / "macos" |
Você compila para esse sistema operacional |
unix / windows |
Você compila para qualquer sistema tipo Unix / para Windows |
target_arch = "x86_64" / "aarch64" / "wasm32" |
Você compila para esse tipo de processador (WebAssembly é wasm32) |
target_pointer_width = "64" |
Os endereços (e o usize) têm 64 bits |
debug_assertions |
É um build de debug (cargo build), não --release |
test |
Você está rodando cargo test |
feature = "json" |
A feature json do Cargo está ligada (mais sobre isso abaixo) |
Quer ver a lista completa da sua máquina? Rode rustc --print cfg. Em um PC Linux de 64 bits, ele imprime linhas como target_os="linux", target_family="unix" e debug_assertions. Você vai ver #[cfg(windows)] e #[cfg(unix)] com frequência em código de FFI, porque bibliotecas em C muitas vezes mudam de um sistema para outro.
#[cfg(test)] e #[cfg_attr]
Seção intitulada “#[cfg(test)] e #[cfg_attr]”Você já usou uma condição sem nem perceber. Todo módulo de testes da página de Testes começa com #[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); }}O cargo test liga o test, então o módulo é compilado e os testes rodam. Um cargo build normal deixa o módulo inteiro de fora, então o seu programa de verdade não carrega nenhum código de teste. O #[cfg] funciona assim em qualquer módulo (veja Módulos), por exemplo #[cfg(windows)] mod windows_paths;.
#[cfg_attr(condição, atributo)] quer dizer: “se a condição for verdadeira, adicione este atributo”. Um uso comum é um derive que você só quer às vezes:
#[cfg_attr(debug_assertions, derive(Debug))]struct Player { name: String, score: u32,}
fn main() { let player = Player { name: "Ana".to_string(), score: 120 };
// só em builds de debug; vai para o stderr, não para o stdout #[cfg(debug_assertions)] eprintln!("debug: {:?}", player);
println!("{} tem {} pontos", player.name, player.score);}Saída:
Ana tem 120 pontosEm um build de debug (o padrão do Playground), você também vê debug: Player { name: "Ana", score: 120 } na parte “Standard Error” da saída. Mude o Playground para “Release” e essa linha some, junto com o código de Debug para Player. Repare que o eprintln! precisa do seu próprio #[cfg(debug_assertions)]. Sem ele, o build de release falha, porque lá Player não implementa Debug. Tem mais sobre builds de debug e de release em Cargo e crates.
Features do Cargo no seu próprio crate
Seção intitulada “Features do Cargo no seu próprio crate”Agora, o cardápio da pizzaria. Uma feature é um interruptor com nome que você define no Cargo.toml, dentro de uma tabela [features]. Vamos montar um programinha, pizza_order, com duas features:
extra_cheese: um adicional grátis, ligado por padrão.json: imprime o pedido em JSON. Ele precisa dos cratesserdeeserde_json, então eles só são baixados e compilados quando alguém pedejson.
[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"]Linha por linha:
optional = truequer dizer “não use este crate, a não ser que alguma feature peça”.extra_cheese = []cria uma feature que não liga mais nada. É só um interruptor que o seu código pode consultar.json = ["dep:serde", "dep:serde_json"]diz: quandojsonestiver ligada, ligue também estas duas dependências opcionais.dep:quer dizer “isto é uma dependência, não o nome de uma feature”.default = [...]lista as features que ficam ligadas quando ninguém diz nada.
No código, você consulta uma feature com feature = "nome":
#[cfg_attr(feature = "json", derive(serde::Serialize))]struct Order { size: String, toppings: Vec<String>,}
fn main() { let mut toppings = vec!["tomate".to_string(), "manjericão".to_string()];
if cfg!(feature = "extra_cheese") { toppings.push("queijo extra".to_string()); }
let order = Order { size: "grande".to_string(), toppings }; println!("Pedido: pizza {} com {}", order.size, order.toppings.join(", "));
#[cfg(feature = "json")] println!("Em JSON: {}", serde_json::to_string(&order).unwrap());}A linha do JSON usa #[cfg], não cfg!. Sem a feature json, o serde_json nem é uma dependência, então essa linha precisa sumir por completo. A verificação do queijo compila de qualquer jeito, então cfg! é suficiente.
Agora faça o seu pedido pela linha de comando. (O -q só esconde as linhas “Compiling…” do Cargo.)
cargo run -q # só as features padrãocargo run -q --features json # as padrão + jsoncargo run -q --no-default-features # nada extra, nem as padrãocargo run -q --all-features # todas as featuresEsta é a saída real:
$ cargo run -qPedido: pizza grande com tomate, manjericão, queijo extra
$ cargo run -q --features jsonPedido: pizza grande com tomate, manjericão, queijo extraEm JSON: {"size":"grande","toppings":["tomate","manjericão","queijo extra"]}
$ cargo run -q --no-default-featuresPedido: pizza grande com tomate, manjericão
$ cargo run -q --all-featuresPedido: pizza grande com tomate, manjericão, queijo extraEm JSON: {"size":"grande","toppings":["tomate","manjericão","queijo extra"]}Dá para combinar as opções. --no-default-features --features json dá JSON sem o queijo. Para várias features, separe com vírgulas: --features json,extra_cheese. As mesmas opções funcionam com cargo build, cargo check e cargo test.
Ligando features das suas dependências
Seção intitulada “Ligando features das suas dependências”Na maior parte do tempo, você vai estar do outro lado do balcão: você usa crates que têm features. Você já viu isso em Cargo e crates:
[dependencies]serde = { version = "1", features = ["derive"] }tokio = { version = "1", features = ["full"] }serde_json = { version = "1", features = ["preserve_order"] }- A feature
derivedoserdete dá o#[derive(Serialize, Deserialize)]. - O
tokionão tem features padrão. Sozinho, ele quase não te dá nada. Afullliga tudo: o runtime,#[tokio::main], timers, rede, arquivos e mais. Isso facilita enquanto você aprende. Em um projeto de verdade, dá para pedir menos, por exemplofeatures = ["rt-multi-thread", "macros"]. - A
preserve_orderdoserde_jsonmantém as chaves dos objetos na ordem em que você as inseriu.
O cargo add escreve essas linhas para você: cargo add tokio --features full.
Por que os crates se dividem assim? Porque cada parte que você não usa é código que você não precisa baixar, compilar nem distribuir. Um crate grande como o tokio deixaria o build de qualquer projetinho lento se sempre compilasse tudo. As features mantêm o tempo de compilação e o tamanho do binário baixos.
Você também pode desligar as features padrão de um crate e escolher só o que precisa, com default-features = false:
[dependencies]serde_json = { version = "1", default-features = false, features = ["alloc"] }Para ver quais features estão ligadas no seu projeto, pergunte ao Cargo:
cargo tree -e features --features json --depth 1Para o pizza_order, isso imprime:
pizza_order v0.1.0 (/home/ana/pizza_order)├── serde feature "default"├── serde feature "derive"└── serde_json feature "default"Sem o --depth 1, você vê a árvore inteira, incluindo as features que as suas dependências ligam nas dependências delas. É longa, mas é a melhor ferramenta quando você se pergunta “por que este crate está sendo compilado?”.
Regras práticas
Seção intitulada “Regras práticas”Features só devem adicionar coisas. Ligar uma feature nunca deve remover uma função nem mudar o que uma função que já existe faz. O motivo é este: se dois crates do seu projeto usam o serde_json, e um pede preserve_order e o outro não, o Cargo compila o serde_json uma vez só, com as features dos dois ligadas. Então todo crate precisa continuar funcionando quando outro crate liga features extras.
Evite features que não podem ficar ligadas juntas. Um design do tipo “feature postgres OU feature sqlite, mas nunca as duas” quebra assim que dois crates escolhem opções diferentes, e o --all-features para de funcionar. Prefira features que possam ficar todas ligadas ao mesmo tempo.
Documente as suas features. Escreva uma lista curta no README ou na documentação do crate: o nome da feature, o que ela adiciona e se ela vem ligada por padrão. No docs.rs, todo crate tem uma página “Feature flags” (por exemplo, a do tokio), e muitos crates marcam itens com “Available on crate feature … only” (disponível só com a feature …).
Teste todas as combinações no CI. O código atrás de uma feature só é compilado quando essa feature está ligada. Coloque estes comandos no seu CI para que nenhum erro fique escondido ali:
cargo check --all-featurescargo check --no-default-featurescargo test --all-featuresEm crates maiores, a ferramenta cargo-hack consegue verificar cada feature separadamente (cargo hack check --each-feature).
Erros comuns
Seção intitulada “Erros comuns”Um erro de digitação no nome da feature. Se você escrever #[cfg(feature = "jsno")], o código continua compilando. A linha do JSON simplesmente nunca é impressa, nem com --features json. Ainda bem que as versões atuais do Rust avisam você:
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 defaultO aviso diz que o valor jsno é inesperado, que existe um com nome parecido ("json") e quais são as features que existem de verdade. Não ignore esse aviso. Na linha de comando, o Cargo é mais rígido: cargo run --features jsno para com error: the package 'pizza_order' does not contain this feature: jsno (o pacote não tem essa feature) e sugere json.
Consultar o nome da dependência em vez do nome da feature. Por causa do dep:serde, não existe nenhuma feature chamada serde no pizza_order. Se você escrever #[cfg_attr(feature = "serde", ...)], recebe o mesmo aviso “unexpected cfg condition value”, desta vez para serde. E aí cargo run --features json falha com o E0277: “the trait bound Order: serde::Serialize is not satisfied” (Order não implementa serde::Serialize), porque o derive nunca foi adicionado. Use o nome que está em [features]: feature = "json".
Esquecer o dep:. Assim que alguma feature usa dep:serde, você precisa usar dep: para o serde em todo lugar. Adicione uma feature nova yaml = ["serde"] e o Cargo se recusa a ler o arquivo:
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.O Cargo explica que serde é uma dependência opcional sem feature implícita e pede para você usar dep:serde. A correção é yaml = ["dep:serde"]. E se você não usar dep: em lugar nenhum, o Cargo cria sem avisar uma feature pública para cada dependência opcional (aqui, --features serde funcionaria). Esse é um estilo antigo. Usar dep: deixa a sua lista de features limpa e intencional.
Código que nunca é compilado. Imagine que você digita errado serde_json::to_strng dentro da linha com #[cfg(feature = "json")]. O cargo check diz que está tudo certo, porque essa linha não é compilada por padrão. Só o cargo check --all-features encontra o erro:
error[E0425]: cannot find function `to_strng` in crate `serde_json` --> src/main.rs:18:41 |18 | println!("Em JSON: {}", serde_json::to_strng(&order).unwrap()); | ^^^^^^^^ |help: a function with a similar name exists |18 | println!("Em JSON: {}", serde_json::to_string(&order).unwrap()); | +O compilador não encontra to_strng no serde_json e sugere to_string. É exatamente por isso que o --all-features tem que estar no seu CI.
Prática
Seção intitulada “Prática”1. Uma pasta de configuração para cada sistema
Seção intitulada “1. Uma pasta de configuração para cada sistema”Escreva uma função config_dir() que retorne onde um app de notas guarda as configurações: "~/.config/notes" no Linux, "~/Library/Application Support/notes" no macOS, r"%APPDATA%\notes" no Windows e "./notes" em qualquer outro sistema. Imprima o resultado no main. Dica: uma função para cada caso com #[cfg(target_os = "...")], e not(any(...)) para a alternativa.
Ver solução
#[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!("As configurações ficam salvas em {}", config_dir());}Saída:
As configurações ficam salvas em ~/.config/notesEm qualquer computador, exatamente uma das quatro funções sobrevive, então o main sempre encontra um config_dir. O Playground roda em Linux, por isso você vê o caminho do Linux. Um app de verdade usaria um crate como o dirs para encontrar essas pastas do jeito certo.
2. Uma exportação opcional para JSON
Seção intitulada “2. Uma exportação opcional para JSON”Você tem um programinha, leaderboard, que imprime pontuações. Adicione uma feature export (desligada por padrão) que traga o serde_json e adicione uma função export_json. Com --features export, o programa também deve imprimir as pontuações em JSON. Dica: deixe o serde_json opcional, use dep:serde_json na feature e coloque #[cfg(feature = "export")] tanto na função quanto na linha que a chama. O Playground não deixa escolher features do Cargo, então teste este na sua máquina com cargo new leaderboard.
Ver solução
[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!("{}: {} pontos", s.name, s.points); }
#[cfg(feature = "export")] println!("Exportado: {}", export_json(&scores));}$ cargo run -qAna: 120 pontosBo: 95 pontos
$ cargo run -q --features exportAna: 120 pontosBo: 95 pontosExportado: [{"name":"Ana","points":120},{"name":"Bo","points":95}]Não tem nenhuma linha default = [...], então export fica desligada a não ser que você peça. Sem ela, o serde_json nem é baixado. Tanto a função quanto a chamada precisam de #[cfg], porque as duas mencionam o serde_json.
#[cfg(condição)]mantém o código só quando a condição é verdadeira. Caso contrário, ele é removido antes de compilar.cfg!(condição)é só umbool. Os dois ramos ainda precisam compilar em todas as plataformas.- Combine condições com
not(..),any(..)eall(..). As mais comuns sãotarget_os,unix/windows,debug_assertionsetest. #[cfg_attr(condição, atributo)]adiciona um atributo só quando a condição é verdadeira.- As features do Cargo ficam em
[features]. Useoptional = truejunto comdep:nomepara crates opcionais, edefault = [...]para o que vem ligado por padrão. - Features só devem adicionar coisas. Rode
cargo check --all-featuresno CI e fique de olho nos avisosunexpected_cfgs.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. O que acontece com uma função marcada com #[cfg(target_os = "windows")] quando você compila no Linux?
2. Por que if cfg!(unix) { ... } não basta para código que usa uma API exclusiva do Unix?
3. No Cargo.toml, o que json = ["dep:serde_json"] faz?
4. Você escreveu #[cfg(feature = "jsno")] sem querer. O que acontece?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”- Features — The Cargo Book (em inglês): o guia completo de
[features], dependências opcionais,dep:e de como as features são unificadas. - Conditional compilation — The Rust Reference (em inglês): todas as opções de
cfg, mais as regras exatas de#[cfg],#[cfg_attr]ecfg!. - Macro
cfg!— biblioteca padrão do Rust (em inglês): o quecfg!retorna e qual a diferença para o atributo#[cfg]. - Specifying Dependencies — The Cargo Book (em inglês): como escrever
features,default-features = falseeoptionalem cada dependência. - cfg — Rust by Example (em inglês): exemplos curtos de
#[cfg]ecfg!lado a lado.