Rust en 2026: Programación de Sistemas, de Ownership a Async
Guía técnica completa que cubre la edición Rust 2024, ownership/borrowing/lifetimes, Cargo y el ecosistema de crates, manejo de errores con Result/anyhow/thiserror, async con Tokio, backends web con Axum, CLIs con clap, WebAssembly, FFI, rendimiento y abstracciones de costo cero, testing, y cuándo elegir Rust sobre Go o Python.
Índice de Contenidos
- Edición Rust 2024 y Fundamentos
- Ownership, Borrowing y Lifetimes
- Cargo, Crates y Estructura de Proyecto
- Manejo de Errores (Result, anyhow, thiserror)
- Async con Tokio
- Backends Web con Axum
- Herramientas CLI con clap
- WebAssembly y FFI
- Rendimiento y Abstracciones de Costo Cero
- Testing y Benchmarking
- Rust en 2026 y Cuándo Elegirlo
1. Edición Rust 2024 y Fundamentos
Ediciones, Toolchain y Sintaxis Moderna
Rust publica una nueva versión estable cada seis semanas y una nueva edición aproximadamente cada tres años. La edición actual es Rust 2024, estabilizada en Rust 1.85 (febrero de 2025) y seleccionada con edition = "2024" en Cargo.toml; las ediciones son opt-in y totalmente interoperables, así que los crates de 2024 enlazan sin problemas con los de 2021. Instala y administra toolchains con rustup, formatea con rustfmt y aplica lints con clippy. Cambios notables de la edición 2024: los bloques extern y los atributos no_mangle/export_name ahora requieren un unsafe explícito, y se ajustaron las reglas de alcance temporal de las expresiones finales. El pattern matching, la verificación de exhaustividad y los tipos de datos algebraicos (enum) son características centrales del lenguaje, no bibliotecas.
// Cargo.toml
// [package]
// name = "metrics"
// edition = "2024" // current edition (Rust 1.85+, Feb 2025)
// rust-version = "1.85" // MSRV pin
use std::collections::HashMap;
// Algebraic data types: enums carry data per variant
#[derive(Debug, Clone)]
enum Event {
Weight { kg: f64 },
BodyFat { pct: f64 },
Unknown(String),
}
// Exhaustive pattern matching with guards and bindings
fn describe(event: &Event) -> String {
match event {
Event::Weight { kg } if *kg > 0.0 => format!("Weight: {kg} kg"),
Event::Weight { .. } => "Invalid weight".to_string(),
Event::BodyFat { pct } => format!("Body fat: {pct}%"),
Event::Unknown(name) => format!("Unknown metric: {name}"),
}
}
// let-else for early returns; if-let chains (2024 edition) for flat control flow
fn parse_kg(raw: &str) -> Option<f64> {
let Ok(v) = raw.trim().parse::<f64>() else {
return None;
};
if v.is_finite() && v > 0.0 { Some(v) } else { None }
}
fn main() {
let events = [Event::Weight { kg: 78.5 }, Event::BodyFat { pct: 15.2 }];
let mut counts: HashMap<&str, u32> = HashMap::new();
for e in &events {
println!("{}", describe(e));
*counts.entry(match e {
Event::Weight { .. } => "weight",
Event::BodyFat { .. } => "body_fat",
Event::Unknown(_) => "unknown",
}).or_insert(0) += 1;
}
println!("{counts:?}");
}
2. Ownership, Borrowing y Lifetimes
Ownership, Movimientos y Borrowing
La idea central de Rust es el ownership: cada valor tiene exactamente un dueño, y cuando el dueño sale de alcance el valor se libera (memoria liberada, archivos cerrados) de forma determinista -- sin recolector de basura, sin free manual. Asignar o pasar un valor que no es Copy lo mueve. En lugar de mover, puedes tomar prestado con referencias: cualquier cantidad de referencias compartidas &T, o exactamente una referencia exclusiva &mut T, nunca ambas a la vez. El borrow checker aplica estas reglas en tiempo de compilación, y así es como Rust garantiza seguridad de memoria y ausencia de data races sin costo en runtime.
#[derive(Debug)]
struct Reading { kg: f64, note: String }
// Takes ownership: `r` is moved in and dropped at the end of this fn.
fn consume(r: Reading) {
println!("consumed {} kg", r.kg);
} // r.note (a heap String) is freed here, automatically
// Shared borrow: read-only, many allowed at once.
fn total(readings: &[Reading]) -> f64 {
readings.iter().map(|r| r.kg).sum()
}
// Exclusive borrow: may mutate, only one at a time.
fn round_all(readings: &mut [Reading]) {
for r in readings.iter_mut() {
r.kg = (r.kg * 10.0).round() / 10.0;
}
}
fn main() {
let mut data = vec![
Reading { kg: 78.53, note: "am".into() },
Reading { kg: 78.61, note: "pm".into() },
];
round_all(&mut data); // exclusive borrow, released after the call
println!("sum = {}", total(&data)); // shared borrow
let first = data.remove(0);
consume(first); // `first` moved out; can't be used afterwards
// println!("{first:?}"); // compile error: value used after move
}
Lifetimes, Slices y Smart Pointers
Un lifetime es el nombre que el compilador le da a "cuánto tiempo es válida una referencia." La mayoría se infieren (elisión de lifetimes), pero cuando una función devuelve una referencia derivada de sus entradas debes anotarla, por ejemplo &'a str, para que el borrow checker pueda demostrar que el resultado nunca sobrevive a su fuente (sin punteros colgantes). Cuando el ownership único es demasiado rígido, usa smart pointers: Box<T> para asignación en el heap, Rc<T>/Arc<T> para ownership compartido (mono- vs multi-hilo), y RefCell<T>/Mutex<T> para mutabilidad interior.
use std::rc::Rc;
use std::cell::RefCell;
// Lifetime 'a ties the returned reference to the longer-lived input.
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() >= b.len() { a } else { b }
}
// Structs that hold references need a lifetime parameter.
struct Parser<'a> {
input: &'a str,
pos: usize,
}
impl<'a> Parser<'a> {
fn rest(&self) -> &'a str { &self.input[self.pos..] }
}
fn main() {
let winner = longest("body-fat", "weight");
println!("longest label: {winner}");
// Shared, mutable, single-threaded state via Rc>.
let shared = Rc::new(RefCell::new(vec![78.5_f64]));
let clone = Rc::clone(&shared);
clone.borrow_mut().push(78.6);
println!("count={} values={:?}",
Rc::strong_count(&shared), shared.borrow());
}
3. Cargo, Crates y Estructura de Proyecto
Cargo: Herramienta de Build y Gestor de Paquetes
Cargo es el sistema de build, el runner de tests y el gestor de paquetes de Rust en una sola herramienta. cargo new crea la estructura de un proyecto, cargo add edita Cargo.toml y elige un rango SemVer compatible, Cargo.lock fija versiones exactas para builds reproducibles, y cargo build --release produce un binario optimizado. Las dependencias vienen de crates.io (o de fuentes git/path). Los feature flags permiten que un crate exponga funcionalidad opcional sin inflar el build de todos. El stack de serialización de facto es serde + serde_json, usado casi en todas partes.
# Scaffold and manage a project
cargo new metrics --bin # or --lib for a library crate
cd metrics
cargo add serde --features derive
cargo add serde_json
cargo add anyhow
cargo build --release # target/release/metrics
cargo run -- --help # args after `--` go to your program
cargo test # runs unit + integration + doctests
cargo clippy --all-targets # lints
cargo fmt # format in place
// Cargo.toml
// [package]
// name = "metrics"
// version = "0.1.0"
// edition = "2024"
//
// [dependencies]
// serde = { version = "1", features = ["derive"] }
// serde_json = "1"
// anyhow = "1"
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize)]
struct BodyMetrics {
weight_kg: f64,
#[serde(skip_serializing_if = "Option::is_none")]
body_fat_pct: Option<f64>,
}
fn main() -> anyhow::Result<()> {
let m = BodyMetrics { weight_kg: 78.5, body_fat_pct: Some(15.2) };
let json = serde_json::to_string_pretty(&m)?; // derive-driven serialization
println!("{json}");
let back: BodyMetrics = serde_json::from_str(&json)?;
println!("parsed {} kg", back.weight_kg);
Ok(())
}
Workspaces, Módulos y Publicación
Un workspace de Cargo agrupa varios crates que comparten un Cargo.lock y un directorio target/ -- ideal para separar un binario, una biblioteca reutilizable y sus tests. Dentro de un crate, el código se organiza en módulos mod con pub controlando la visibilidad. Publicar en crates.io es cargo publish; SemVer es un contrato social estricto, y cargo semver-checks detecta cambios incompatibles accidentales antes de publicar.
# Workspace Cargo.toml (repo root)
[workspace]
resolver = "3" # 2024-edition dependency resolver
members = ["app", "core", "cli"]
[workspace.dependencies] # centralize versions once
serde = { version = "1", features = ["derive"] }
tokio = { version = "1", features = ["full"] }
// core/src/lib.rs -- module tree and visibility
pub mod metrics {
#[derive(Debug, Clone, Copy)]
pub struct Bmi(pub f64);
/// Body-mass index from weight (kg) and height (m).
pub fn bmi(weight_kg: f64, height_m: f64) -> Bmi {
Bmi(weight_kg / (height_m * height_m))
}
// private helper: not exported outside this module
fn _classify(_b: Bmi) -> &'static str { "normal" }
}
// A doc-test: `cargo test` compiles and runs this snippet.
/// ```
/// let b = core::metrics::bmi(78.5, 1.74);
/// assert!((b.0 - 25.9).abs() < 0.1);
/// ```
pub fn _doc_anchor() {}
4. Manejo de Errores (Result, anyhow, thiserror)
Result, Option y el Operador ?
Rust no tiene excepciones. Las fallas recuperables son valores: Result<T, E> (un Ok o un Err) y Option<T> (un Some o None). El operador ? propaga los errores hacia arriba -- retorna temprano ante Err/None y en caso contrario extrae el valor -- para que el camino feliz del código quede plano. Los bugs verdaderamente irrecuperables usan panic! (o .unwrap()/.expect()), que desenrolla el hilo. El compilador te obliga a manejar cada Result, así que los caminos de error no se pueden ignorar en silencio.
use std::num::ParseFloatError;
// The `?` operator converts and propagates the error automatically.
fn parse_weight(raw: &str) -> Result<f64, ParseFloatError> {
let kg: f64 = raw.trim().parse()?; // early-return on Err
Ok(kg)
}
// Combinators keep transformations concise without unwrapping.
fn valid_bmi(weight: &str, height_m: f64) -> Option<f64> {
weight.trim().parse::<f64>().ok() // Result -> Option
.filter(|kg| *kg > 0.0)
.map(|kg| kg / (height_m * height_m))
}
fn main() {
match parse_weight("78.5") {
Ok(kg) => println!("ok: {kg} kg"),
Err(e) => eprintln!("bad input: {e}"),
}
// Provide a fallback instead of panicking:
let kg = parse_weight("N/A").unwrap_or(0.0);
println!("fallback kg = {kg}");
println!("bmi = {:?}", valid_bmi("78.5", 1.74));
}
anyhow y thiserror
Dos crates dominan el manejo de errores. Usa thiserror (2.x) en bibliotecas para derivar un enum de error preciso y tipado con conversiones #[from] y mensajes Display. Usa anyhow (1.x) en aplicaciones para un anyhow::Error encapsulado que absorbe cualquier tipo de error, agrega .context() con pistas legibles para humanos e imprime la cadena completa de causas. La regla práctica: las bibliotecas exponen errores tipados, los binarios los colapsan con anyhow::Result.
// Library-side: a precise, typed error with thiserror 2.x
use thiserror::Error;
#[derive(Debug, Error)]
pub enum MetricError {
#[error("failed to read {path}")]
Io { path: String, #[source] source: std::io::Error },
#[error("invalid number: {0}")]
Parse(#[from] std::num::ParseFloatError), // auto From impl for `?`
#[error("weight {0} kg is out of range")]
OutOfRange(f64),
}
pub fn load_weight(path: &str) -> Result<f64, MetricError> {
let text = std::fs::read_to_string(path)
.map_err(|source| MetricError::Io { path: path.into(), source })?;
let kg: f64 = text.trim().parse()?; // ParseFloatError -> MetricError
if !(30.0..=300.0).contains(&kg) {
return Err(MetricError::OutOfRange(kg));
}
Ok(kg)
}
// Application-side: anyhow with context breadcrumbs
use anyhow::{Context, Result};
fn run() -> Result<()> {
let kg = load_weight("today.txt")
.context("loading today's weigh-in")?; // adds a cause layer
println!("weight: {kg} kg");
Ok(())
}
fn main() -> Result<()> {
run() // returning Result from main prints the full error chain
}
5. Async con Tokio
El Runtime Tokio y async/await
El async/await de Rust es de costo cero: una async fn se compila a una máquina de estados que implementa el trait Future, y nada se ejecuta hasta que un runtime la sondea. Tokio (1.x) es el runtime async dominante -- un planificador multi-hilo con work-stealing más TCP/UDP async, timers, sistema de archivos y primitivas de sincronización. Anota #[tokio::main] en main, lanza trabajo concurrente con tokio::spawn, y usa .await para ceder en lugar de bloquear. Clientes HTTP como reqwest (0.12) se construyen sobre él.
// Cargo.toml
// tokio = { version = "1", features = ["full"] }
// reqwest = { version = "0.12", features = ["json"] }
// anyhow = "1"
use anyhow::Result;
async fn fetch_len(url: &str) -> Result<usize> {
let body = reqwest::get(url).await?.text().await?;
Ok(body.len())
}
#[tokio::main]
async fn main() -> Result<()> {
let urls = ["https://example.com", "https://www.rust-lang.org"];
// Spawn tasks: they run concurrently on the runtime's thread pool.
let handles: Vec<_> = urls.iter()
.map(|u| { let u = u.to_string(); tokio::spawn(async move { fetch_len(&u).await }) })
.collect();
for h in handles {
match h.await? { // join the task, then unwrap its Result
Ok(n) => println!("{n} bytes"),
Err(e) => eprintln!("request failed: {e}"),
}
}
Ok(())
}
Tasks, Canales y select!
Las tasks se comunican por canales en lugar de estado mutable compartido: mpsc para pipelines de muchos productores/un consumidor, oneshot para una única respuesta, broadcast/watch para fan-out. La macro tokio::select! espera varios futures a la vez y actúa sobre el que se complete primero -- la forma idiomática de agregar timeouts, cancelación y apagado ordenado. Este modelo de paso de mensajes compone limpiamente y evita la mayoría de los locks.
use tokio::sync::mpsc;
use tokio::time::{sleep, Duration};
#[tokio::main]
async fn main() {
let (tx, mut rx) = mpsc::channel::<f64>(32);
// Producer task
let producer = tokio::spawn(async move {
for kg in [78.5, 78.4, 78.6] {
tx.send(kg).await.expect("receiver dropped");
sleep(Duration::from_millis(50)).await;
}
// tx dropped here -> channel closes
});
// Consumer with a timeout guard via select!
loop {
tokio::select! {
maybe = rx.recv() => match maybe {
Some(kg) => println!("got {kg} kg"),
None => break, // channel closed
},
_ = sleep(Duration::from_secs(1)) => {
eprintln!("timeout waiting for data");
break;
}
}
}
producer.await.unwrap();
}
Estado Compartido, Streams y Límites de Concurrencia
Cuando debes compartir estado, envuélvelo en Arc<Mutex<T>> (o el RwLock optimizado para lecturas); usa tokio::sync::Mutex solo si un lock se mantiene a través de un .await. Limita el trabajo en vuelo con un Semaphore, y procesa secuencias por demanda con Streams de futures/tokio-stream. Para un fan-out estructurado y seguro ante cancelación, JoinSet recolecta muchas tasks y entrega resultados a medida que terminan.
use std::sync::Arc;
use tokio::sync::Semaphore;
use tokio::task::JoinSet;
#[tokio::main]
async fn main() {
// At most 4 concurrent operations, regardless of how many we spawn.
let limit = Arc::new(Semaphore::new(4));
let mut set = JoinSet::new();
for id in 0..20 {
let permit = Arc::clone(&limit);
set.spawn(async move {
let _p = permit.acquire().await.unwrap(); // held for the task
tokio::time::sleep(std::time::Duration::from_millis(20)).await;
id * id
});
}
let mut total = 0u64;
while let Some(res) = set.join_next().await {
total += res.unwrap() as u64; // results arrive as tasks finish
}
println!("sum of squares = {total}");
}
6. Backends Web con Axum
Axum: Routing, Handlers y JSON
Axum (0.8) es el framework web ergonómico del equipo de Tokio, construido sobre hyper y la abstracción de servicios tower. Los handlers son funciones async normales cuyos argumentos son extractors (parámetros de ruta, query, cuerpo JSON, estado compartido) y cuyo tipo de retorno implementa IntoResponse. El routing es verificado por tipos y sin macros. Ten en cuenta que la sintaxis de rutas de 0.8 usa llaves -- /users/{id}, no el antiguo /users/:id. Combínalo con serde para JSON y sqlx (0.8) para SQL verificado en compilación.
// axum = "0.8"; tokio = { version = "1", features = ["full"] }
// serde = { version = "1", features = ["derive"] }
use axum::{Router, routing::{get, post}, Json, extract::Path};
use axum::http::StatusCode;
use serde::{Deserialize, Serialize};
#[derive(Serialize)]
struct Metric { id: u64, weight_kg: f64 }
#[derive(Deserialize)]
struct NewMetric { weight_kg: f64 }
async fn get_metric(Path(id): Path<u64>) -> Json<Metric> {
Json(Metric { id, weight_kg: 78.5 })
}
async fn create_metric(Json(body): Json<NewMetric>) -> (StatusCode, Json<Metric>) {
let m = Metric { id: 1, weight_kg: body.weight_kg };
(StatusCode::CREATED, Json(m))
}
#[tokio::main]
async fn main() {
let app = Router::new()
.route("/api/metrics/{id}", get(get_metric)) // 0.8 brace syntax
.route("/api/metrics", post(create_metric));
let listener = tokio::net::TcpListener::bind("0.0.0.0:8000").await.unwrap();
axum::serve(listener, app).await.unwrap();
}
Estado, Extractors, Middleware y Errores
Comparte un pool de conexiones o configuración con el extractor State y .with_state(). Compón preocupaciones transversales -- tracing, CORS, compresión, timeouts, autenticación -- como capas tower/tower-http aplicadas en una sola cadena .layer(). Para handlers falibles, devuelve Result<T, AppError> e implementa IntoResponse para tu tipo de error, de modo que las fallas se mapeen a códigos de estado HTTP limpios y cuerpos JSON.
use axum::{Router, routing::get, Json, extract::State};
use axum::response::{IntoResponse, Response};
use axum::http::StatusCode;
use std::sync::Arc;
use tower_http::trace::TraceLayer;
#[derive(Clone)]
struct AppState { pool: Arc<sqlx::PgPool> }
// A custom error that turns into an HTTP response.
enum AppError { NotFound, Db(sqlx::Error) }
impl IntoResponse for AppError {
fn into_response(self) -> Response {
let (code, msg) = match self {
AppError::NotFound => (StatusCode::NOT_FOUND, "not found"),
AppError::Db(_) => (StatusCode::INTERNAL_SERVER_ERROR, "db error"),
};
(code, Json(serde_json::json!({ "error": msg }))).into_response()
}
}
impl From<sqlx::Error> for AppError { fn from(e: sqlx::Error) -> Self { AppError::Db(e) } }
async fn latest(State(st): State<AppState>) -> Result<Json<f64>, AppError> {
let kg: Option<f64> = sqlx::query_scalar("SELECT weight_kg FROM metrics ORDER BY id DESC LIMIT 1")
.fetch_optional(&*st.pool).await?; // sqlx::Error -> AppError via `?`
kg.map(Json).ok_or(AppError::NotFound)
}
pub fn router(state: AppState) -> Router {
Router::new()
.route("/api/latest", get(latest))
.layer(TraceLayer::new_for_http()) // request/response tracing
.with_state(state)
}
7. Herramientas CLI con clap
clap: Parsing Declarativo de Argumentos
clap (4.x) es el estándar para parsing de línea de comandos. Su API derive convierte un struct simple en un parser completo: flags, opciones, argumentos posicionales, valores por defecto, validación, respaldos por variable de entorno, y --help/--version autogenerados. Los subcomandos se mapean naturalmente a un enum. Las CLIs de Rust compilan a un único binario estático sin runtime que instalar -- por eso tantas herramientas de desarrollo modernas (ripgrep, fd, bat, uv) están escritas en Rust.
// clap = { version = "4", features = ["derive", "env"] }
use clap::{Parser, Subcommand, ValueEnum};
use std::path::PathBuf;
#[derive(Parser)]
#[command(name = "metrics", version, about = "Health metrics CLI")]
struct Cli {
/// Increase logging verbosity (-v, -vv)
#[arg(short, long, action = clap::ArgAction::Count)]
verbose: u8,
#[command(subcommand)]
command: Command,
}
#[derive(Subcommand)]
enum Command {
/// Export metrics from a directory
Export {
#[arg(value_name = "DIR")]
directory: PathBuf,
#[arg(short, long, value_enum, default_value_t = Format::Json)]
format: Format,
/// Days of history (env: METRICS_DAYS)
#[arg(short, long, env = "METRICS_DAYS", default_value_t = 30)]
days: u32,
},
}
#[derive(Clone, ValueEnum)]
enum Format { Json, Csv }
fn main() {
let cli = Cli::parse(); // exits with a nice error on bad input
match cli.command {
Command::Export { directory, format, days } => {
let fmt = match format { Format::Json => "json", Format::Csv => "csv" };
println!("exporting {days} days from {} as {fmt}", directory.display());
}
}
}
CLIs Robustas: Errores, Salida y Configuración
Las CLIs de producción combinan clap con un puñado de crates: anyhow para un error de nivel superior amigable (devuelve Result desde main y sale con código distinto de cero automáticamente), tracing + tracing-subscriber para logs estructurados y filtrados por nivel, indicatif para barras de progreso, y serde + toml/figment para configuración por capas (defaults < archivo < env < flags). Envía los diagnósticos a stderr y la salida legible por máquina a stdout para que la herramienta se componga en pipelines.
use anyhow::{Context, Result};
use tracing::{info, Level};
fn init_logging(verbose: u8) {
let level = match verbose { 0 => Level::WARN, 1 => Level::INFO, _ => Level::DEBUG };
tracing_subscriber::fmt()
.with_max_level(level)
.with_writer(std::io::stderr) // logs to stderr, data to stdout
.init();
}
fn run() -> Result<()> {
let raw = std::fs::read_to_string("metrics.toml")
.context("reading metrics.toml")?; // anyhow adds context to the chain
info!(bytes = raw.len(), "loaded config");
// ... do work, write results to stdout ...
println!("{{\"status\":\"ok\"}}");
Ok(())
}
fn main() -> Result<()> {
init_logging(1);
run().context("metrics export failed") // non-zero exit + full cause chain
}
8. WebAssembly y FFI
WebAssembly con wasm-bindgen
Rust es un lenguaje de primera clase para WebAssembly. Compila a wasm32-unknown-unknown para el navegador (con wasm-bindgen generando el glue de JS y wasm-pack el paquete npm), o a wasm32-wasip2 para el modelo de componentes WASI usado por runtimes de edge/serverless. Wasm te da cómputo casi nativo y en sandbox dentro del navegador -- ideal para loops calientes (procesamiento de imágenes, parsing, criptografía) que serían lentos en JavaScript, sin GC y con un .wasm diminuto.
// Cargo.toml
// [lib]
// crate-type = ["cdylib"]
// [dependencies]
// wasm-bindgen = "0.2"
use wasm_bindgen::prelude::*;
// Exported to JS as a plain function.
#[wasm_bindgen]
pub fn bmi(weight_kg: f64, height_m: f64) -> f64 {
weight_kg / (height_m * height_m)
}
// Exported struct becomes a JS class.
#[wasm_bindgen]
pub struct Smoother { window: usize, buf: Vec<f64> }
#[wasm_bindgen]
impl Smoother {
#[wasm_bindgen(constructor)]
pub fn new(window: usize) -> Smoother { Smoother { window, buf: vec![] } }
pub fn push(&mut self, v: f64) -> f64 {
self.buf.push(v);
if self.buf.len() > self.window { self.buf.remove(0); }
self.buf.iter().sum::<f64>() / self.buf.len() as f64
}
}
// Build: wasm-pack build --target web
// JS: import init, { bmi, Smoother } from "./pkg/metrics.js";
// await init(); bmi(78.5, 1.74);
FFI: Llamar a C y Ser Llamado desde Otros Lenguajes
Rust interopera con C sin costo. Llama a C con un bloque unsafe extern "C" (los bloques extern requieren unsafe en la edición 2024), y expón Rust a C, Python (vía PyO3), Node o Go construyendo un cdylib con funciones #[unsafe(no_mangle)] extern "C" -- nota que la edición 2024 envuelve no_mangle en unsafe(...). Usa std::ffi (CStr/CString) para puentear tipos de cadenas de forma segura, y bindgen/cbindgen para autogenerar headers en cualquier dirección.
use std::ffi::{c_char, c_double, CStr};
// 1) Calling a C library from Rust (2024 edition: `unsafe extern`).
unsafe extern "C" {
fn cbrt(x: c_double) -> c_double; // from libm
}
fn cube_root(x: f64) -> f64 {
unsafe { cbrt(x) } // FFI calls are unsafe
}
// 2) Exposing a Rust function to C / Python / Node as a cdylib.
// Cargo.toml: [lib] crate-type = ["cdylib"]
#[unsafe(no_mangle)] // 2024 edition: unsafe(...) wrapper
pub extern "C" fn metrics_bmi(weight_kg: c_double, height_m: c_double) -> c_double {
weight_kg / (height_m * height_m)
}
/// Accept a C string safely across the boundary.
/// # Safety
/// `name` must be a valid, NUL-terminated C string pointer.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn greet_len(name: *const c_char) -> usize {
if name.is_null() { return 0; }
let s = unsafe { CStr::from_ptr(name) };
s.to_bytes().len()
}
fn main() { println!("cbrt(27) = {}", cube_root(27.0)); }
9. Rendimiento y Abstracciones de Costo Cero
Traits, Genéricos y Abstracciones de Costo Cero
Las abstracciones de Rust desaparecen al compilar. Las funciones genéricas y las cotas de trait se monomorfizan -- el compilador genera una copia especializada e inlineable por cada tipo concreto, así que una llamada genérica es tan rápida como una escrita a mano y sin vtable. Elige despacho estático (impl Trait / <T: Trait>) para velocidad, o despacho dinámico (dyn Trait detrás de un puntero) cuando necesites colecciones heterogéneas y aceptes una indirección. Los traits también habilitan la sobrecarga de operadores, los iteradores y las conversiones From/Into que se usan en todas partes.
trait Metric {
fn value(&self) -> f64;
fn unit(&self) -> &'static str;
}
struct Weight(f64);
struct BodyFat(f64);
impl Metric for Weight { fn value(&self)->f64{self.0} fn unit(&self)->&'static str{"kg"} }
impl Metric for BodyFat { fn value(&self)->f64{self.0} fn unit(&self)->&'static str{"%"} }
// Static dispatch: monomorphized, inlined, zero overhead.
fn describe<M: Metric>(m: &M) -> String {
format!("{}{}", m.value(), m.unit())
}
// Dynamic dispatch: one vtable indirection, heterogeneous storage.
fn describe_all(items: &[Box<dyn Metric>]) -> Vec<String> {
items.iter().map(|m| format!("{}{}", m.value(), m.unit())).collect()
}
fn main() {
println!("{}", describe(&Weight(78.5)));
let mixed: Vec<Box<dyn Metric>> = vec![Box::new(Weight(78.5)), Box::new(BodyFat(15.2))];
println!("{:?}", describe_all(&mixed));
}
Iteradores: Perezosos, Fusionados y Sin Asignaciones
Las cadenas de iteradores son el ejemplo canónico de una abstracción de costo cero: map/filter/fold son adaptadores perezosos que el optimizador fusiona en un solo loop sin asignaciones intermedias, igualando o superando a un for escrito a mano. Prefiere tomar prestado (&str, slices) y usar adaptadores de iterador en lugar de clonar. Cuando paralelices, el crate rayon convierte .iter() en .par_iter() para acelerar con paralelismo de datos manteniendo las mismas garantías de seguridad.
// A single fused loop, no temporary Vecs allocated.
fn mean_valid(readings: &[f64]) -> Option<f64> {
let (sum, n) = readings.iter()
.copied()
.filter(|kg| kg.is_finite() && *kg > 0.0)
.fold((0.0, 0u32), |(s, c), kg| (s + kg, c + 1));
(n > 0).then(|| sum / n as f64)
}
// Data parallelism with rayon (rayon = "1"): same result, many cores.
// use rayon::prelude::*;
// let total: f64 = big_slice.par_iter().map(|r| r.abs()).sum();
fn main() {
let data = [78.5, f64::NAN, -1.0, 78.6, 78.4];
println!("mean = {:?}", mean_valid(&data)); // Some(78.5)
}
Builds de Release, Profiling y unsafe/SIMD
Mide siempre en --release: habilita las optimizaciones que hacen desaparecer las abstracciones. Ajusta el perfil de release en Cargo.toml (LTO, codegen-units=1, panic="abort") para exprimir el último porcentaje, y define RUSTFLAGS="-C target-cpu=native" para desbloquear el SIMD del host. Perfila con cargo flamegraph o samply antes de optimizar. Para el raro punto caliente que las abstracciones seguras no pueden expresar, entra a un bloque unsafe pequeño y bien auditado -- o usa SIMD portable vía la API nightly std::simd o el crate estable wide.
# Cargo.toml -- release profile tuning
[profile.release]
opt-level = 3
lto = "thin" # link-time optimization across crates
codegen-units = 1 # better optimization, slower compile
panic = "abort" # smaller/faster; no unwinding
# Build and measure
cargo build --release
RUSTFLAGS="-C target-cpu=native" cargo build --release
cargo install flamegraph && cargo flamegraph --bin metrics
// Minimal, contained unsafe: skip redundant bounds checks in a proven-safe loop.
pub fn dot(a: &[f32], b: &[f32]) -> f32 {
assert_eq!(a.len(), b.len());
let mut acc = 0.0f32;
for i in 0..a.len() {
// SAFETY: i < a.len() == b.len(), enforced by the assert above.
acc += unsafe { a.get_unchecked(i) * b.get_unchecked(i) };
}
acc
}
10. Testing y Benchmarking
Tests Unitarios, de Integración y de Documentación
El testing está integrado en el lenguaje y en Cargo. Los tests unitarios viven junto al código en un bloque #[cfg(test)] mod tests (pueden probar elementos privados); los tests de integración viven en tests/ y ejercen solo la API pública; y los ejemplos en comentarios de documentación funcionan además como tests, así que tus docs nunca quedan desactualizados. Corre todo con cargo test, o usa el runner paralelo más rápido cargo nextest run. Los tests async usan #[tokio::test].
pub fn bmi(weight_kg: f64, height_m: f64) -> f64 {
weight_kg / (height_m * height_m)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn computes_bmi() {
let b = bmi(78.5, 1.74);
assert!((b - 25.93).abs() < 0.01);
}
#[test]
#[should_panic(expected = "divide")]
fn zero_height_panics() {
// demonstrate an expected panic path
assert!(bmi(78.5, 0.0).is_finite(), "divide by zero");
}
#[tokio::test] // async test needs tokio's test macro
async fn fetches() {
let n = super::async_len().await;
assert!(n >= 0);
}
}
async fn async_len() -> usize { 3 }
Benchmarks, Property Tests y CI
Para microbenchmarks con rigor estadístico, usa criterion (0.5+), que calienta, muestrea y detecta regresiones entre corridas; en nightly, #[bench]/std::hint::black_box también funcionan. proptest y quickcheck generan entradas aleatorias para encontrar casos límite, y cargo fuzz impulsa fuzzing guiado por cobertura. En CI, condiciona los merges a cargo fmt --check, cargo clippy -- -D warnings, cargo test, y cargo deny para chequeos de cadena de suministro/licencias.
// benches/bmi.rs -- criterion = "0.5" (dev-dependency)
use criterion::{criterion_group, criterion_main, Criterion, black_box};
fn bench_bmi(c: &mut Criterion) {
c.bench_function("bmi", |b| {
b.iter(|| my_crate::bmi(black_box(78.5), black_box(1.74)))
});
}
criterion_group!(benches, bench_bmi);
criterion_main!(benches);
# Property test with proptest (proptest = "1")
# proptest! { #[test] fn bmi_positive(w in 1.0f64..300.0, h in 0.5f64..2.5) {
# prop_assert!(my_crate::bmi(w, h) > 0.0);
# }}
# CI gate (GitHub Actions snippet)
# - run: cargo fmt --all -- --check
# - run: cargo clippy --all-targets -- -D warnings
# - run: cargo nextest run --all-features
# - run: cargo deny check # licenses + advisories
# - run: cargo bench --no-run # ensure benches compile
11. Rust en 2026 y Cuándo Elegirlo
Cadencia de Versiones y el Sistema de Ediciones
Rust publica una versión estable cada seis semanas, así que para mediados de 2026 el toolchain va por la serie 1.9x, todo construido desde los mismos trenes que nightly y beta. La estabilidad sin estancamiento viene de las ediciones: épocas opt-in del lenguaje (2015, 2018, 2021, y la actual 2024, estabilizada en Rust 1.85, feb 2025) que pueden hacer pequeños cambios de sintaxis incompatibles mientras cada crate sigue compilando, porque las ediciones interoperan en la frontera del crate. Cambias de edición en tu propio calendario con cargo fix --edition. La próxima edición se espera alrededor de 2027 en la cadencia de aproximadamente tres años.
# Manage toolchains and migrate editions safely
rustup update stable # latest 1.9x (six-week cadence)
rustup toolchain install nightly
rustc --version # e.g. rustc 1.9x.0 (2026)
# Migrate a crate from the 2021 to the 2024 edition
cargo fix --edition # applies mechanical fixes
# then set edition = "2024" in Cargo.toml and re-run tests
# rust-toolchain.toml pins the toolchain per-repo for reproducible builds
# [toolchain]
# channel = "1.85"
# components = ["clippy", "rustfmt"]
Estabilizaciones Recientes: async en Traits, Closures async, gen
La historia de async ha madurado. async fn en traits (AFIT) y impl Trait en posición de retorno dentro de traits (RPITIT) son estables desde Rust 1.75, así que puedes escribir trait Repo { async fn get(&self) -> T; } para traits internos sin la macro async-trait. Los closures async se estabilizaron con la edición 2024 en Rust 1.85, permitiendo que los closures capturen y hagan .await de forma natural. Las let-chains (if let ... && let ...) llegaron con la edición 2024 en la serie 1.8x. Los generadores (bloques gen que producen iteradores) y los traits async totalmente compatibles con dyn todavía se están estabilizando -- para traits async seguros como objetos en bibliotecas, #[trait_variant] o async-trait siguen siendo el puente pragmático.
// async fn in traits (AFIT) -- stable since Rust 1.75, no macro needed
trait MetricStore {
async fn latest(&self) -> Option<f64>; // desugars to RPITIT
async fn save(&self, kg: f64) -> anyhow::Result<()>;
}
struct MemStore { last: std::sync::Mutex<Option<f64>> }
impl MetricStore for MemStore {
async fn latest(&self) -> Option<f64> { *self.last.lock().unwrap() }
async fn save(&self, kg: f64) -> anyhow::Result<()> {
*self.last.lock().unwrap() = Some(kg);
Ok(())
}
}
// async closure (Rust 1.85 / 2024 edition): captures + .await inline
async fn run_twice<F>(mut f: F) where F: AsyncFnMut() {
f().await; f().await;
}
Cuándo Elegir Rust en vez de Go
Go y Rust se solapan en servicios de backend y CLIs, pero optimizan cosas distintas. Go gana en velocidad de iteración: un lenguaje simple, un recolector de basura, goroutines y compilaciones famosamente rápidas lo hacen excelente para equipos que envían servicios de red estándar con rapidez. Rust gana cuando necesitas latencia predecible (sin pausas de GC), máximo throughput por núcleo, presupuestos de memoria ajustados, o garantías en tiempo de compilación contra data races y errores de memoria -- bases de datos, proxies, embebidos, motores de juegos, WASM e infraestructura sensible a la latencia. La curva de aprendizaje de Rust y sus compilaciones más largas son el precio de ese control. Un patrón común: escribir la mayoría de los servicios en Go, y reescribir en Rust el camino caliente ya comprobado.
Cuándo Elegir Rust en vez de Python (y Cómo se Combinan)
Python es insuperable para exploración, ciencia de datos, pegamento entre sistemas y orquestación de AI/ML, gracias a su ecosistema y a la velocidad de desarrollo tipo REPL -- pero es lento y su historia de concurrencia (incluso con los builds free-threaded de 3.13+) es limitada. Elige Rust para núcleos intensivos en CPU, servicios de alta concurrencia, software de sistemas, y en cualquier lugar donde importen la correctitud y el uso de recursos. Los dos se combinan de maravilla: conserva la experiencia de desarrollo de Python y baja el camino caliente a una extensión de Rust vía PyO3 + maturin, que es exactamente cómo herramientas como uv, ruff, polars y pydantic-core logran aceleraciones de 10-100x mientras presentan una API normal de Python.
// Rust function exposed to Python with PyO3 + maturin
// Cargo.toml: [lib] crate-type = ["cdylib"]
// [dependencies] pyo3 = { version = "0.24", features = ["extension-module"] }
use pyo3::prelude::*;
/// A hot loop written in Rust, called from Python as `fastmath.mean(xs)`.
#[pyfunction]
fn mean(xs: Vec<f64>) -> f64 {
if xs.is_empty() { return 0.0; }
xs.iter().sum::<f64>() / xs.len() as f64
}
#[pymodule]
fn fastmath(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(mean, m)?)?;
Ok(())
}
// Build + install into the active venv: maturin develop --release
// Python: import fastmath; fastmath.mean([78.5, 78.6, 78.4])