Tu primer programa y la anatomía de un crate
src/main.rs frente a src/lib.rs, qué es fn main, y el modelo de compilación de Rust: se compilan crates, no archivos sueltos. La diferencia entre paquete y crate, explicada de verdad.
Rust no compila archivos: compila crates. Esta frase, que suena a tecnicismo, es en realidad el modelo mental que separa a quien entiende el lenguaje de quien solo lo copia. Antes de escribir tu primer fn main, conviene saber qué es exactamente lo que el compilador mira.
- Escribir y ejecutar tu primer programa Rust.
- Distinguir
src/main.rsdesrc/lib.rs. - Separar los conceptos de paquete y crate.
- Entender el modelo de compilación por crate, no por archivo.
Tu primer programa
Dentro de un paquete recién creado, src/main.rs contiene:
fn main() {
println!("Hola, Rust 2024.");
}
cargo run
# Hola, Rust 2024.
fn main es el punto de entrada del programa: el runtime lo llama al arrancar. println! no es una función, sino una macro (lo delata el !): se expande en tiempo de compilación y comprueba el formato de la cadena contra sus argumentos, así que un error de formato es un error de compilación, no un fallo en ejecución.
Paquete y crate: no son lo mismo
Dos palabras que los novatos confunden y que tienen definiciones precisas:
Paquete (package)
Lo que tiene un Cargo.toml. Agrupa uno o más crates y define sus dependencias y metadatos.
Crate
La unidad de compilación: lo mínimo que el compilador considera de una vez. Puede ser binario o librería.
Crate binario
Tiene un fn main y compila a un ejecutable. Su raíz es src/main.rs.
Crate librería
No tiene main; ofrece código reutilizable y compila a un .rlib. Su raíz es src/lib.rs.
Las reglas de un paquete: como mucho un crate librería, cualquier número de crates binarios (cada archivo en src/bin/ es uno más), y al menos un crate. Un paquete con main.rs y lib.rs a la vez es de lo más normal: la lógica vive en la librería y el binario es una fina capa que la invoca.
flowchart TD P[Paquete y su Cargo.toml] --> CB[Crate binario raiz main.rs] P --> CL[Crate libreria raiz lib.rs] CL --> M1[modulo red] CL --> M2[modulo db] M2 --> M3[submodulo pool]
Rust compila crates, no archivos
Aquí está la diferencia profunda con C. En C, cada .c es una translation unit que se compila por separado y luego el linker une los trozos; por eso hacen falta cabeceras que declaren lo que vive en otro archivo. En Rust no hay nada de eso:
- El compilador toma la raíz del crate (
main.rsolib.rs) y desde ahí ve el crate entero, con todos sus módulos a la vez. - Los módulos (
mod red;) no son unidades de compilación: son solo la forma de organizar un espacio de nombres dentro del mismo crate. No existen las cabeceras ni las declaraciones adelantadas.
mod red; // carga src/red.rs
mod db; // carga src/db.rs o src/db/mod.rs
pub fn arrancar() {
red::escuchar();
db::conectar();
}
Como el compilador ve todo el crate junto, puede razonar globalmente: comprobar tipos y lifetimes a través de módulos, inlinear con libertad y monomorfizar genéricos. La compilación incremental que hace cargo es una caché sobre este modelo, no un cambio del modelo: la unidad semántica sigue siendo el crate.
Dentro del crate, los elementos son privados por defecto: solo se ven en su módulo y en los descendientes. pub abre esa visibilidad, y las rutas nombran dónde vive cada cosa con crate::, super:: y self:::
pub mod red {
pub fn escuchar() {
conf::puerto(); // ruta relativa al módulo actual
}
mod conf {
pub fn puerto() -> u16 {
super::valor_defecto() // super:: sube un nivel
}
}
fn valor_defecto() -> u16 {
8080
}
}
// Desde fuera, la ruta absoluta arranca en la raíz con crate::
fn arranca() {
crate::red::escuchar();
}
Este árbol de módulos es puramente organizativo: no genera fronteras de compilación, solo de visibilidad. Es el sistema de privacidad de Rust, y opera enteramente dentro del mismo crate.
La verdadera frontera de compilación separada está entre crates, no entre archivos. Cada dependencia de Cargo.toml se compila a su .rlib con sus metadatos, y tu crate se apoya en ellos. Por eso recompilar tras tocar una línea es rápido: solo tu crate se rehace, no todo el árbol de dependencias.
fn main y el código de salida
main puede no devolver nada o devolver un Result, lo que permite usar el operador ? para propagar errores en el propio arranque:
use std::fs;
fn main() -> Result<(), Box<dyn std::error::Error>> {
let texto = fs::read_to_string("config.toml")?; // ? propaga el error
println!("{} bytes de config", texto.len());
Ok(())
}
Si main devuelve Ok, el proceso termina con código 0; si devuelve Err, Rust imprime el error y sale con un código distinto de cero. Esto lo gobierna el trait Termination, que conecta el valor de retorno de main con el código de salida del sistema operativo.
Quien llega de C o de Java arrastra el modelo “un archivo, una unidad” y lo proyecta sobre Rust, donde no aplica. En Rust, dividir el código en más archivos no cambia nada de la compilación: mod solo mueve nombres de sitio dentro del mismo crate, que se compila entero de un golpe. Esto tiene consecuencias enormes. No hay cabeceras que mantener sincronizadas, no hay orden de declaración que respetar, no hay #include que dependa de guardas. El compilador conoce todo el crate y por eso sus mensajes de error pueden cruzar módulos, sus optimizaciones no se frenan en fronteras de archivo, y el borrow checker razona sobre el conjunto. Cuando quieras acelerar la compilación de un proyecto grande, la palanca no es partir archivos: es partir crates, porque son ellos —y no los archivos— los que se compilan en paralelo y se cachean por separado. Pensar en crates, y no en archivos, es empezar a pensar como el compilador.
- Ejecuta tu primer
fn mainconcargo runy cambia el mensaje. - Convierte
mainpara que devuelvaResulte intenta leer un archivo que no existe: observa el código de salida conecho $?. - Crea un
src/lib.rs, mueve una función allí y llámala desdemain.rs. - Añade un módulo
mod util;en un archivosrc/util.rsy usa una función suya. - Crea un segundo binario en
src/bin/otro.rsy ejecútalo concargo run --bin otro.