Ir al contenido

Cargo y crates en Rust

En la página Empezar con Rust usaste cargo new y cargo run. Eso fue solo el principio. Cargo es la herramienta de compilación de Rust y también su gestor de paquetes. Compila tu código, ejecuta tus pruebas, da formato a tus archivos y descarga bibliotecas que han escrito otras personas.

Esas bibliotecas se llaman crates (paquetes de código). Piensa en los crates como las apps de una tienda de aplicaciones: alguien ya resolvió un problema (leer JSON, hacer peticiones web, procesar fechas) y tú puedes añadir su solución a tu proyecto con un solo comando. En esta página verás cómo funciona y cómo elegir un buen crate.

Hay dos palabras que aparecen todo el tiempo:

  • Un crate es una unidad de código Rust que compila el compilador. Puede ser un binario (un programa que puedes ejecutar, que empieza en src/main.rs) o una biblioteca (código para que lo usen otros programas, que empieza en src/lib.rs).
  • Un paquete (package) es lo que crea cargo new: una carpeta con un archivo Cargo.toml y uno o más crates dentro.

En el día a día, la gente dice “crate” para las dos cosas. Cuando alguien dice “usa el crate serde”, quiere decir “añade el paquete serde de crates.io a tu proyecto”.

Cada paquete tiene un archivo Cargo.toml. Está escrito en TOML, un formato de configuración sencillo. Esto es lo que te da cargo new hello_rust:

[package]
name = "hello_rust"
version = "0.1.0"
edition = "2024"
[dependencies]
  • [package] describe tu proyecto. name y version son lo que vería la gente si lo publicaras.
  • edition elige qué edición del lenguaje Rust usar. Las ediciones (2018, 2021, 2024) permiten que Rust mejore sin romper el código antiguo. Deja la que haya escrito cargo new.
  • [dependencies] enumera los crates que usa tu código. Al principio está vacía.

A medida que el proyecto crece, puedes ver más secciones:

[dependencies]
serde = { version = "1.0", features = ["derive"] }
serde_json = "1.0"
[dev-dependencies]
# solo lo usan las pruebas y los ejemplos, no tu programa real
pretty_assertions = "1.4"
[profile.release]
# configuración para `cargo build --release`
opt-level = 3

Puedes editar [dependencies] a mano, pero lo más fácil es cargo add. Busca la última versión en crates.io y escribe la línea por ti:

Terminal window
cargo add serde_json

Algunos crates tienen partes opcionales llamadas features (funcionalidades opcionales). Vienen desactivadas por defecto para que tu compilación sea más ligera. Las activas con --features:

Terminal window
cargo add serde --features derive

Después de estos dos comandos, tu Cargo.toml tendrá algo parecido a esto:

[dependencies]
serde = { version = "1.0.228", features = ["derive"] }
serde_json = "1.0.145"

Ahora usa el crate en tu código. El siguiente cargo run lo descarga, lo compila una vez y lo enlaza con tu programa:

use serde_json::json;
fn main() {
let crab = json!({ "legs": 10, "name": "Ferris" });
println!("{}", crab);
println!("Nombre: {}", crab["name"]);
}

Salida:

{"legs":10,"name":"Ferris"}
Nombre: "Ferris"
▶ Pruébalo tú mismo

Las comillas alrededor de "Ferris" aparecen porque crab["name"] sigue siendo un valor JSON, no un string normal de Rust.

El Playground de Rust ya incluye los crates más populares, así que este ejemplo funciona ahí sin cargo add. En tu propio equipo, primero necesitas el paso cargo add serde_json. Aprenderás mucho más sobre este crate en la página JSON con Serde.

Para quitar un crate que ya no necesitas, ejecuta cargo remove serde_json.

Cuando escribes serde_json = "1.0", no estás pidiendo exactamente la versión 1.0. Estás diciendo “cualquier versión compatible con la 1.0”. Cargo sigue el versionado semántico (SemVer): una versión tiene la forma MAJOR.MINOR.PATCH (mayor, menor, parche), y solo un cambio en el número mayor puede romper tu código.

Escribes Cargo puede usar Por qué
"1.0" (igual que "^1.0") >=1.0.0 y <2.0.0 todas las versiones 1.x deberían ser compatibles
"1.4.2" >=1.4.2 y <2.0.0 al menos la 1.4.2, pero siempre 1.x
"0.4" >=0.4.0 y <0.5.0 antes de la 1.0, el número menor cuenta como cambio incompatible
"~1.4" >=1.4.0 y <1.5.0 solo actualizaciones de parche
"=1.4.2" exactamente 1.4.2 casi nunca hace falta

Entonces, ¿quién decide la versión exacta? Ese es el trabajo de Cargo.lock. La primera vez que compilas, Cargo elige las versiones más nuevas que cumplen tus requisitos y las apunta en Cargo.lock. A partir de ahí, todas las compilaciones usan exactamente esas mismas versiones, en tu equipo y en el de cualquier otra persona.

  • No edites Cargo.lock a mano. Cargo se encarga de él.
  • Súbelo a git, sobre todo si es un programa. Así tus compañeros y tu CI compilan exactamente lo mismo que tú.
  • cargo update pasa a las versiones más nuevas que sigan cumpliendo Cargo.toml y reescribe Cargo.lock. Nunca salta a una versión mayor nueva; para eso tienes que cambiar Cargo.toml tú mismo.

Hay tres sitios web que usarás una y otra vez:

  • crates.io es el registro oficial. Todos los crates públicos están aquí. Busca una palabra como “csv” o “date”.
  • docs.rs aloja la documentación de todos los crates, generada automáticamente. Por ejemplo, docs.rs/serde_json.
  • lib.rs es un índice no oficial y más rápido que agrupa los crates por categorías. Es ideal para explorar.

Muchos crates resuelven el mismo problema, así que ¿cómo eliges? Antes de añadir uno, tómate un minuto para revisar:

  1. Descargas. Los crates populares tienen millones de descargas. Eso significa que mucha gente encontró y corrigió los errores antes que tú.
  2. Actualizaciones recientes. Mira la fecha de la última versión. Un crate que no ha cambiado en años puede estar bien (algunos simplemente están “terminados”), pero comprueba que su repositorio no esté lleno de issues sin respuesta.
  3. Documentación. Abre su página en docs.rs. Los buenos crates tienen una portada clara con un ejemplo que puedes copiar.
  4. Versión 1.0 o superior. No es obligatorio, pero te indica que el autor promete una API estable.
  5. Pocas dependencias. Cada crate que añades trae sus propias dependencias. Cuanto más pequeño, más fácil es confiar en él.

Si tienes dudas, busca “rust” más tu problema y mira qué crate recomienda la comunidad una y otra vez.

No hace falta que te los aprendas de memoria. Ten esta tabla a mano y los irás aprendiendo con el uso:

Comando Qué hace
cargo new my_app Crea un proyecto binario nuevo (añade --lib para una biblioteca)
cargo build Compila en target/debug/
cargo run Compila y ejecuta (los argumentos van después de --: cargo run -- input.txt)
cargo check Busca errores sin generar el programa. Mucho más rápido que build
cargo test Ejecuta tus pruebas (consulta Pruebas en Rust)
cargo fmt Da formato a todo tu código con el estilo estándar
cargo clippy Ejecuta el linter. Te sugiere código más simple e idiomático
cargo doc --open Genera la documentación HTML de tu proyecto y de cada dependencia, y la abre
cargo update Actualiza Cargo.lock a las versiones compatibles más nuevas
cargo build --release Compila un programa optimizado en target/release/

Un buen hábito: ejecuta cargo check a menudo mientras escribes, y cargo fmt y cargo clippy antes de hacer commit. cargo doc --open también es una joya escondida: te da la documentación de las versiones exactas de los crates que usas, y funciona sin conexión.

Por defecto, Cargo hace una compilación debug (de depuración). Compila rápido y mantiene comprobaciones de seguridad extra, como provocar un panic cuando un entero se desborda. Es perfecta mientras desarrollas.

Una compilación release (cargo build --release o cargo run --release) tarda más en compilar, pero se ejecuta mucho más rápido, a menudo 10 veces o más. Úsala cuando distribuyas tu programa o midas su velocidad.

Tu código incluso puede saber qué tipo de compilación es:

fn main() {
if cfg!(debug_assertions) {
println!("Compilación debug: las comprobaciones extra están activadas");
} else {
println!("Compilación release: optimizada para la velocidad");
}
}

Salida:

Compilación debug: las comprobaciones extra están activadas
▶ Pruébalo tú mismo

En el Playground puedes cambiar de “Debug” a “Release” en el menú y volver a ejecutarlo para ver la otra línea.

Un paquete puede contener una biblioteca y un binario al mismo tiempo. Es una organización muy común: pones la lógica de verdad en src/lib.rs, y src/main.rs se queda pequeño y solo la llama:

greeter/
├── Cargo.toml
└── src/
├── lib.rs # el crate de biblioteca: lógica reutilizable
└── main.rs # el crate binario: el programa que ejecutas
src/lib.rs
pub fn greet(name: &str) -> String {
format!("¡Hola, {name}!")
}
src/main.rs
fn main() {
// la biblioteca se llama igual que el paquete
println!("{}", greeter::greet("Ferris"));
}

¿Por qué dividirlo así? El código de lib.rs es fácil de probar, y otros programas (o un segundo binario) pueden reutilizarlo. El Playground solo tiene un archivo, pero puedes conseguir algo parecido con un pub mod, como en la página de Módulos en Rust:

mod greeter {
pub fn greet(name: &str) -> String {
format!("¡Hola, {name}!")
}
}
fn main() {
println!("{}", greeter::greet("Ferris"));
}

Salida:

¡Hola, Ferris!
▶ Pruébalo tú mismo

Cuando un proyecto crece hasta tener varios paquetes que van juntos (por ejemplo, una app de línea de comandos, un servidor web y una biblioteca compartida), puedes ponerlos en un workspace (espacio de trabajo). Comparten un único Cargo.lock y una única carpeta target/, así que todo se compila junto y usa las mismas versiones de las dependencias.

El Cargo.toml principal enumera los miembros:

[workspace]
members = ["app", "core"]
resolver = "3"
my_project/
├── Cargo.toml # el archivo del workspace de arriba
├── Cargo.lock # compartido por todos los miembros
├── app/ # un paquete binario
└── core/ # un paquete de biblioteca

Así, cargo build en la carpeta principal lo compila todo, y cargo run -p app ejecuta un solo miembro. No lo necesitarás en tus primeros proyectos, pero viene bien reconocerlo cuando leas código de otras personas.

Usar un crate sin añadirlo. Si escribes use serde_json::json; pero olvidaste cargo add serde_json, el compilador no lo encuentra:

error[E0432]: unresolved import `serde_json`
--> src/main.rs:1:5
|
1 | use serde_json::json;
| ^^^^^^^^^^ use of unresolved module or unlinked crate `serde_json`
|
= help: you might be missing a crate named `serde_json`

El compilador te dice que no puede resolver la importación serde_json y que quizá te falta un crate con ese nombre. La solución es ejecutar cargo add serde_json. Revisa también cómo está escrito: los nombres de los crates usan - o _, y en el código un - siempre se convierte en _.

Olvidar una feature. Si #[derive(Serialize)] dice que no encuentra la macro derive, probablemente añadiste serde sin --features derive. Vuelve a ejecutar cargo add serde --features derive; actualiza la línea que ya existe.

Medir la velocidad con una compilación debug. Si tu programa parece lento, prueba cargo run --release antes de cambiar nada del código.

Usa la macro json! de serde_json para describir un crate con tres claves: downloads (un número), name y version. Imprime el valor completo y después solo la versión.

Pista: copia la forma del ejemplo de crab en Añadir una dependencia con cargo add. En tu equipo, ejecuta antes cargo add serde_json; el Playground ya lo incluye.

Ver solución
use serde_json::json;
fn main() {
let krate = json!({
"downloads": 500,
"name": "serde_json",
"version": "1.0"
});
println!("{}", krate);
println!("Versión: {}", krate["version"]);
}

Salida:

{"downloads":500,"name":"serde_json","version":"1.0"}
Versión: "1.0"
▶ Pruébalo tú mismo

La variable se llama krate porque crate es una palabra reservada en Rust. Esa forma de escribirlo es una broma habitual en el código Rust. Igual que antes, la versión sale entre comillas porque sigue siendo un valor JSON; la página JSON con Serde explica cómo obtener el texto sin comillas.

Escribe una función debug_log(msg: &str) que imprima [debug] seguido del mensaje, pero solo en una compilación debug. Úsala en main alrededor de un cálculo pequeño e imprime el resultado de forma normal.

Pista: envuelve el println! en if cfg!(debug_assertions) { ... }, como en Compilación debug y release.

Ver solución
fn debug_log(msg: &str) {
if cfg!(debug_assertions) {
println!("[debug] {msg}");
}
}
fn main() {
debug_log("empieza el cálculo");
let mut total = 0;
for n in 1..=10 {
total += n;
}
debug_log("cálculo terminado");
println!("La suma de 1 a 10 es {total}");
}

Salida:

[debug] empieza el cálculo
[debug] cálculo terminado
La suma de 1 a 10 es 55
▶ Pruébalo tú mismo

Cambia el Playground a “Release” (o usa cargo run --release en tu equipo) y vuelve a ejecutarlo: solo se imprime la última línea. Para proyectos reales, la página de Logging muestra crates que hacen esto como es debido, con niveles como debug e info.

  • Cargo compila, ejecuta, prueba y da formato a tu código, y descarga crates de crates.io.
  • Cargo.toml es lo que pides; Cargo.lock apunta las versiones exactas que obtuviste. Sube los dos a git.
  • cargo add nombre --features x añade una dependencia. "1.0" significa “cualquier versión 1.x compatible”.
  • Juzga un crate por sus descargas, sus actualizaciones recientes y una buena documentación en docs.rs.
  • Usa cargo check mientras escribes, cargo fmt y cargo clippy antes de hacer commit, y --release cuando importe la velocidad.
  • Pon la lógica reutilizable en src/lib.rs y mantén src/main.rs pequeño.

Comprueba lo aprendido

1. En Cargo.toml, ¿qué significa serde_json = "1.0"?

2. ¿Qué archivo apunta las versiones exactas de cada dependencia que usó tu compilación?

3. Quieres buscar errores en tu código lo más rápido posible, sin generar un programa. ¿Qué comando es el más adecuado?

4. ¿Cómo añades serde con su feature derive activada?

Puntuación: 0 / 4