Features de Cargo y compilación condicional (#[cfg]) en Rust
Piensa en pedir una pizza. Todas llevan la base y la salsa de tomate. Después eliges los extras: champiñones, queso extra, aceitunas. La cocina solo prepara lo que pediste, así que no pagas (ni esperas) por ingredientes que no quieres.
El código de Rust funciona igual. Con la compilación condicional (#[cfg]), le dices al compilador “construye esta parte solo en Windows” o “solo en las pruebas”. Con las features de Cargo (funcionalidades opcionales), quienes usan tu crate eligen los extras que necesitan, como el soporte para JSON. El código que nadie eligió no se compila en absoluto: no hace más lenta la compilación y no acaba en el programa final.
Esta página se apoya en Cargo y crates (Cargo.toml, cargo add) y en Herramientas de Rust. Si esas palabras te suenan nuevas, échales un vistazo rápido primero.
#[cfg]: conservar o quitar código
Sección titulada «#[cfg]: conservar o quitar código»#[cfg(...)] es un atributo que pones sobre una función, un struct, una línea use, un módulo o incluso una sola instrucción. Si la condición es verdadera, el código se queda. Si es falsa, el compilador lo elimina por completo, como si nunca lo hubieras 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!("¡Hola desde {}!", os_name());}Salida:
¡Hola desde Linux!El Rust Playground se ejecuta en Linux, así que imprime ¡Hola desde Linux!. En tu portátil con Windows, el mismo código imprime ¡Hola desde Windows!. Hay tres funciones con el mismo nombre, pero no pasa nada: en cada computadora solo sobrevive una de ellas.
Aquí hay una pequeña trampa. En FreeBSD ninguna de las tres condiciones es verdadera, así que os_name no existe y la compilación se detiene con E0425: “cannot find function os_name in this scope” (no se encuentra la función os_name en este ámbito). En las siguientes secciones verás cómo añadir una alternativa.
cfg!(): un simple verdadero o falso
Sección titulada «cfg!(): un simple verdadero o falso»A veces no quieres quitar código. Solo quieres hacer una pregunta. La macro cfg!() acepta las mismas condiciones, pero te devuelve un simple bool:
fn main() { let bits = if cfg!(target_pointer_width = "64") { 64 } else { 32 }; println!("Este es un programa de {bits} bits"); println!("El usize más grande es {}", usize::MAX);
if cfg!(target_os = "windows") { println!("Las rutas se ven así: C:\\Users\\ana"); } else { println!("Las rutas se ven así: /home/ana"); }}Salida:
Este es un programa de 64 bitsEl usize más grande es 18446744073709551615Las rutas se ven así: /home/anatarget_pointer_width es el tamaño de una dirección de memoria: "64" en casi cualquier computadora moderna y "32" en algunos dispositivos pequeños. También es el tamaño de usize, y por eso usize::MAX es tan grande aquí.
Entonces, ¿cuándo usar cada uno?
#[cfg(...)] |
cfg!(...) |
|
|---|---|---|
| Qué hace | Elimina el código cuando es falso | Se convierte en true o false |
| ¿El código “falso” se sigue compilando? | No, desaparece | Sí, las dos ramas deben compilar en todas partes |
| Úsalo para | Código que no puede compilar en otros sitios (APIs de un solo sistema, crates opcionales) | Pequeñas diferencias de comportamiento |
La fila del medio es importante. Cosas exclusivas de Unix, como std::os::unix::fs::PermissionsExt, no existen en Windows, así que el código que las usa tiene que ir detrás de #[cfg(unix)]. Un if cfg!(unix) no basta, porque el compilador de Windows igualmente intentaría compilar esa rama.
Combinar condiciones: not, any, all
Sección titulada «Combinar condiciones: not, any, all»Puedes construir condiciones más grandes a partir de otras pequeñas:
not(a): es verdadera cuandoaes falsaany(a, b, ...): es verdadera cuando al menos una lo es (como “o”)all(a, b, ...): es verdadera cuando todas lo son (como “y”)
Aquí tienes otra vez el ejemplo de los sistemas operativos, esta vez con una alternativa para que compile en todas partes:
#[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 { "desconocida"}
fn main() { println!("Shell por defecto: {}", shell());
if cfg!(all(unix, target_pointer_width = "64")) { println!("Un sistema Unix de 64 bits"); }}Salida:
Shell por defecto: shUn sistema Unix de 64 bitsunix y windows son nombres cortos muy prácticos. unix es verdadero en Linux, macOS, los BSD y sistemas parecidos. Estas son las condiciones que verás más a menudo:
| Condición | Es verdadera cuando… |
|---|---|
target_os = "linux" / "windows" / "macos" |
Compilas para ese sistema operativo |
unix / windows |
Compilas para cualquier sistema tipo Unix / para Windows |
target_arch = "x86_64" / "aarch64" / "wasm32" |
Compilas para ese tipo de procesador (WebAssembly es wasm32) |
target_pointer_width = "64" |
Las direcciones (y usize) son de 64 bits |
debug_assertions |
Es una compilación debug (cargo build), no --release |
test |
Estás ejecutando cargo test |
feature = "json" |
La feature json de Cargo está activada (más abajo lo verás) |
¿Quieres ver la lista completa para tu máquina? Ejecuta rustc --print cfg. En un PC Linux de 64 bits imprime líneas como target_os="linux", target_family="unix" y debug_assertions. Verás #[cfg(windows)] y #[cfg(unix)] muy a menudo en el código de FFI, porque las bibliotecas de C suelen cambiar de un sistema a otro.
#[cfg(test)] y #[cfg_attr]
Sección titulada «#[cfg(test)] y #[cfg_attr]»Ya usaste una condición sin darte cuenta. Todos los módulos de pruebas de la página de Pruebas empiezan 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 activa test, así que el módulo se compila y sus pruebas se ejecutan. Un cargo build normal deja fuera el módulo entero, así que tu programa real no lleva nada de código de pruebas. #[cfg] funciona así con cualquier módulo (mira Módulos), por ejemplo #[cfg(windows)] mod windows_paths;.
#[cfg_attr(condición, atributo)] significa: “si la condición es verdadera, añade este atributo”. Un uso habitual es un derive que solo quieres a veces:
#[cfg_attr(debug_assertions, derive(Debug))]struct Player { name: String, score: u32,}
fn main() { let player = Player { name: "Ana".to_string(), score: 120 };
// solo en compilaciones debug; va a stderr, no a stdout #[cfg(debug_assertions)] eprintln!("debug: {:?}", player);
println!("{} tiene {} puntos", player.name, player.score);}Salida:
Ana tiene 120 puntosEn una compilación debug (la opción por defecto del Playground), también verás debug: Player { name: "Ana", score: 120 } en la parte “Standard Error” de la salida. Cambia el Playground a “Release” y esa línea desaparece, junto con el código de Debug para Player. Fíjate en que el eprintln! necesita su propio #[cfg(debug_assertions)]. Sin él, la compilación release falla, porque ahí Player no implementa Debug. Tienes más sobre las compilaciones debug y release en Cargo y crates.
Features de Cargo en tu propio crate
Sección titulada «Features de Cargo en tu propio crate»Ahora, el menú de pizzas. Una feature es un interruptor con nombre que defines en Cargo.toml, dentro de una tabla [features]. Vamos a construir un pequeño programa, pizza_order, con dos features:
extra_cheese: un extra gratis, activado por defecto.json: imprime el pedido en JSON. Necesita los cratesserdeyserde_json, así que solo se descargan y se compilan cuando alguien pidejson.
[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"]Línea por línea:
optional = truesignifica “no uses este crate a menos que una feature lo pida”.extra_cheese = []crea una feature que no activa nada más. Es solo un interruptor que tu código puede consultar.json = ["dep:serde", "dep:serde_json"]dice: cuandojsonesté activada, activa también estas dos dependencias opcionales.dep:significa “esto es una dependencia, no el nombre de una feature”.default = [...]enumera las features que están activadas cuando nadie dice lo contrario.
En el código, compruebas una feature con feature = "nombre":
#[cfg_attr(feature = "json", derive(serde::Serialize))]struct Order { size: String, toppings: Vec<String>,}
fn main() { let mut toppings = vec!["tomate".to_string(), "albahaca".to_string()];
if cfg!(feature = "extra_cheese") { toppings.push("queso extra".to_string()); }
let order = Order { size: "grande".to_string(), toppings }; println!("Pedido: pizza {} con {}", order.size, order.toppings.join(", "));
#[cfg(feature = "json")] println!("En JSON: {}", serde_json::to_string(&order).unwrap());}La línea del JSON usa #[cfg], no cfg!. Sin la feature json, serde_json ni siquiera es una dependencia, así que esa línea tiene que desaparecer por completo. La comprobación del queso compila bien en cualquier caso, así que cfg! es suficiente.
Ahora pide tu pizza desde la línea de comandos. (-q solo oculta las líneas “Compiling…” de Cargo.)
cargo run -q # solo las features por defectocargo run -q --features json # las de por defecto + jsoncargo run -q --no-default-features # nada extra, ni siquiera las de por defectocargo run -q --all-features # todas las featuresEsta es la salida real:
$ cargo run -qPedido: pizza grande con tomate, albahaca, queso extra
$ cargo run -q --features jsonPedido: pizza grande con tomate, albahaca, queso extraEn JSON: {"size":"grande","toppings":["tomate","albahaca","queso extra"]}
$ cargo run -q --no-default-featuresPedido: pizza grande con tomate, albahaca
$ cargo run -q --all-featuresPedido: pizza grande con tomate, albahaca, queso extraEn JSON: {"size":"grande","toppings":["tomate","albahaca","queso extra"]}Puedes combinar las opciones. --no-default-features --features json te da JSON sin el queso. Para varias features, sepáralas con comas: --features json,extra_cheese. Las mismas opciones funcionan con cargo build, cargo check y cargo test.
Activar features de tus dependencias
Sección titulada «Activar features de tus dependencias»La mayor parte del tiempo estarás al otro lado del mostrador: usas crates que tienen features. Ya lo viste en Cargo y crates:
[dependencies]serde = { version = "1", features = ["derive"] }tokio = { version = "1", features = ["full"] }serde_json = { version = "1", features = ["preserve_order"] }- La feature
derivedeserdete da#[derive(Serialize, Deserialize)]. tokiono tiene features por defecto. Por sí solo no te da casi nada.fulllo activa todo: el runtime,#[tokio::main], temporizadores, red, archivos y más. Es cómodo mientras aprendes. En un proyecto real puedes pedir menos, por ejemplofeatures = ["rt-multi-thread", "macros"].preserve_orderdeserde_jsonmantiene las claves de los objetos en el orden en que las insertaste.
cargo add escribe estas líneas por ti: cargo add tokio --features full.
¿Por qué los crates se dividen así? Porque cada parte que no usas es código que no tienes que descargar, compilar ni distribuir. Un crate grande como tokio haría que cualquier proyecto pequeño tardara mucho en compilar si siempre lo compilara todo. Las features mantienen bajos los tiempos de compilación y el tamaño de los binarios.
También puedes desactivar las features por defecto de un crate y elegir solo lo que necesitas, con default-features = false:
[dependencies]serde_json = { version = "1", default-features = false, features = ["alloc"] }Para ver qué features están activadas en tu proyecto, pregúntale a Cargo:
cargo tree -e features --features json --depth 1Para pizza_order, esto imprime:
pizza_order v0.1.0 (/home/ana/pizza_order)├── serde feature "default"├── serde feature "derive"└── serde_json feature "default"Sin --depth 1 ves el árbol completo, incluidas las features que tus dependencias activan en sus propias dependencias. Es largo, pero es la mejor herramienta cuando te preguntas “¿por qué se está compilando este crate?”.
Reglas prácticas
Sección titulada «Reglas prácticas»Las features solo deberían añadir cosas. Activar una feature nunca debería quitar una función ni cambiar lo que hace una función que ya existe. El motivo es este: si dos crates de tu proyecto usan serde_json, y uno pide preserve_order y el otro no, Cargo compila serde_json una sola vez, con las features de ambos activadas. Así que cada crate tiene que seguir funcionando cuando otro activa features extra.
Evita features que no pueden estar activadas a la vez. Un diseño como “feature postgres O feature sqlite, pero nunca las dos” se rompe en cuanto dos crates eligen una distinta, y --all-features deja de funcionar. Mejor usa features que puedan estar todas activadas al mismo tiempo.
Documenta tus features. Escribe una lista corta en tu README o en la documentación de tu crate: el nombre de la feature, qué añade y si está activada por defecto. En docs.rs, cada crate tiene una página “Feature flags” (por ejemplo, la de tokio), y muchos crates marcan elementos con “Available on crate feature … only” (disponible solo con la feature …).
Comprueba todas las combinaciones en CI. El código detrás de una feature solo se compila cuando esa feature está activada. Añade esto a tu CI para que no se esconda nada ahí:
cargo check --all-featurescargo check --no-default-featurescargo test --all-featuresEn crates más grandes, la herramienta cargo-hack puede comprobar cada feature por separado (cargo hack check --each-feature).
Errores comunes
Sección titulada «Errores comunes»Un error de escritura en el nombre de una feature. Si escribes #[cfg(feature = "jsno")], el código sigue compilando. La línea del JSON simplemente nunca se imprime, ni siquiera con --features json. Por suerte, las versiones actuales de Rust te avisan:
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 defaultEl aviso dice que el valor jsno es inesperado, que hay uno con un nombre parecido ("json") y cuáles son las features que sí existen. No lo ignores. En la línea de comandos, Cargo es más estricto: cargo run --features jsno se detiene con error: the package 'pizza_order' does not contain this feature: jsno (el paquete no tiene esa feature) y te sugiere json.
Comprobar el nombre de la dependencia en lugar del de la feature. Por culpa de dep:serde, en pizza_order no existe ninguna feature llamada serde. Si escribes #[cfg_attr(feature = "serde", ...)], recibes el mismo aviso “unexpected cfg condition value”, esta vez para serde. Y luego cargo run --features json falla con E0277: “the trait bound Order: serde::Serialize is not satisfied” (Order no implementa serde::Serialize), porque el derive nunca se añadió. Usa el nombre que aparece en [features]: feature = "json".
Olvidar dep:. En cuanto una feature dice dep:serde, tienes que usar dep: para serde en todas partes. Añade una feature nueva yaml = ["serde"] y Cargo se niega a leer el archivo:
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 te explica que serde es una dependencia opcional sin feature implícita y te pide usar dep:serde. La solución es yaml = ["dep:serde"]. Y si no pones dep: en ningún sitio, Cargo crea en silencio una feature pública para cada dependencia opcional (aquí funcionaría --features serde). Ese es un estilo antiguo. Usar dep: mantiene tu lista de features limpia e intencionada.
Código que nunca se compila. Imagina que escribes mal serde_json::to_strng dentro de la línea con #[cfg(feature = "json")]. cargo check dice que todo está bien, porque esa línea no se compila por defecto. Solo cargo check --all-features lo encuentra:
error[E0425]: cannot find function `to_strng` in crate `serde_json` --> src/main.rs:18:41 |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()); | +El compilador no encuentra to_strng en serde_json y te sugiere to_string. Precisamente por esto --all-features tiene que estar en tu CI.
Práctica
Sección titulada «Práctica»1. Una carpeta de configuración para cada sistema
Sección titulada «1. Una carpeta de configuración para cada sistema»Escribe una función config_dir() que devuelva dónde guarda su configuración una app de notas: "~/.config/notes" en Linux, "~/Library/Application Support/notes" en macOS, r"%APPDATA%\notes" en Windows y "./notes" en cualquier otro sistema. Imprime el resultado en main. Pista: una función por caso con #[cfg(target_os = "...")], y not(any(...)) para la alternativa.
Ver solución
#[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!("La configuración se guarda en {}", config_dir());}Salida:
La configuración se guarda en ~/.config/notesEn cada computadora sobrevive exactamente una de las cuatro funciones, así que main siempre encuentra un config_dir. El Playground usa Linux, así que ves la ruta de Linux. Una app real usaría un crate como dirs para encontrar estas carpetas como es debido.
2. Una exportación a JSON opcional
Sección titulada «2. Una exportación a JSON opcional»Tienes un pequeño programa, leaderboard, que imprime puntuaciones. Añade una feature export (desactivada por defecto) que traiga serde_json y añada una función export_json. Con --features export, el programa también debe imprimir las puntuaciones en JSON. Pista: haz que serde_json sea opcional, usa dep:serde_json en la feature y pon #[cfg(feature = "export")] tanto en la función como en la línea que la llama. El Playground no permite elegir features de Cargo, así que prueba este en tu máquina con cargo new leaderboard.
Ver solución
[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!("{}: {} puntos", s.name, s.points); }
#[cfg(feature = "export")] println!("Exportado: {}", export_json(&scores));}$ cargo run -qAna: 120 puntosBo: 95 puntos
$ cargo run -q --features exportAna: 120 puntosBo: 95 puntosExportado: [{"name":"Ana","points":120},{"name":"Bo","points":95}]No hay ninguna línea default = [...], así que export está desactivada a menos que la pidas. Sin ella, serde_json ni siquiera se descarga. Tanto la función como la llamada necesitan #[cfg], porque las dos mencionan serde_json.
Resumen
Sección titulada «Resumen»#[cfg(condición)]conserva el código solo cuando la condición es verdadera. Si no, lo elimina antes de compilar.cfg!(condición)es solo unbool. Las dos ramas tienen que compilar en todas las plataformas.- Combina condiciones con
not(..),any(..)yall(..). Las más comunes sontarget_os,unix/windows,debug_assertionsytest. #[cfg_attr(condición, atributo)]añade un atributo solo cuando la condición es verdadera.- Las features de Cargo viven en
[features]. Usaoptional = truejunto condep:nombrepara los crates opcionales, ydefault = [...]para lo que está activado por defecto. - Las features solo deberían añadir cosas. Ejecuta
cargo check --all-featuresen CI y presta atención a los avisosunexpected_cfgs.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. ¿Qué le pasa a una función marcada con #[cfg(target_os = "windows")] cuando compilas en Linux?
2. ¿Por qué if cfg!(unix) { ... } no basta para código que usa una API exclusiva de Unix?
3. En Cargo.toml, ¿qué hace json = ["dep:serde_json"]?
4. Escribiste #[cfg(feature = "jsno")] por error. ¿Qué pasa?
Puntuación: 0 / 4
Para leer más
Sección titulada «Para leer más»- Features — The Cargo Book (en inglés): la guía completa de
[features], las dependencias opcionales,dep:y cómo se unifican las features. - Conditional compilation — The Rust Reference (en inglés): todas las opciones de
cfg, y las reglas exactas de#[cfg],#[cfg_attr]ycfg!. - Macro
cfg!— biblioteca estándar de Rust (en inglés): qué devuelvecfg!y en qué se diferencia del atributo#[cfg]. - Specifying Dependencies — The Cargo Book (en inglés): cómo escribir
features,default-features = falseyoptionalen cada dependencia. - cfg — Rust by Example (en inglés): ejemplos cortos de
#[cfg]ycfg!uno al lado del otro.