El crate alloc: recuperar Vec, Box y String sin std
Entre la austeridad de `core` y la plenitud de `std` hay un piso intermedio: `alloc`. Devuelve los tipos que viven en el montículo —`Box`, `Vec`, `String`, `Rc`— a cambio de una sola cosa: que tú proveas un asignador global implementando `GlobalAlloc`. Ni SO ni runtime: solo memoria y reglas para repartirla. La descomposición en tres capas que hace de Rust un lenguaje que paga por lo que su entorno ofrece.
no_std te quita el montículo, y con él Vec, Box y String. Pero fíjate en la razón exacta por la que se fueron: no porque necesiten un sistema operativo, sino porque necesitan asignar memoria dinámica, y sin std no hay quien la reparta. Esas son dos exigencias distintas, y Rust las separa con precisión de cirujano. Entre core —que no asigna nada— y std —que asume un SO entero— existe un tercer crate, alloc, que contiene justo los tipos del montículo y pide a cambio una única cosa: que tú digas de dónde sale la memoria. Ni ficheros, ni hilos, ni red: solo un asignador. Provéelo, y recuperas Vec y String en un microcontrolador que jamás ha oído hablar de un sistema operativo.
- Situar
alloccomo la capa intermedia entrecoreystd, ni una ni otra. - Activar
allocen un crateno_stdconextern crate alloc;y recuperarVec,BoxyString. - Definir un
#[global_allocator]que implemente el traitGlobalAllocconallocydealloc. - Saber qué devuelve
allocy qué se queda fuera pese al montículo, comoHashMap.
La capa intermedia
alloc es un crate del sysroot, hermano de core y std, que contiene exactamente los tipos que necesitan un montículo y nada más del sistema. Es más: el propio std toma Box, Vec y String de alloc y los reexporta. std::vec::Vec es alloc::vec::Vec. La pila de capas queda así, y tú eliges en qué piso te plantas:
flowchart LR CORE[core sin asignador] --> ALLOC[core mas alloc heap con asignador propio] ALLOC --> STD[std heap mas sistema operativo] style CORE fill:#a6e3a1,color:#11111b style ALLOC fill:#cba6f7,color:#11111b style STD fill:#89b4fa,color:#11111b
core no presupone nada. core más alloc presupone que existe un asignador, pero no un SO. std presupone las dos cosas. La programación de sistemas consiste, en buena medida, en elegir conscientemente el piso mínimo que tu entorno puede sostener.
Activar alloc y designar un asignador
Recuperar el montículo son dos pasos. El primero: nombrar el crate. alloc no se enlaza automáticamente en no_std ni está en el prelude, así que lo declaras de forma explícita con una de las pocas apariciones legítimas de extern crate que quedan en el Rust moderno:
#![no_std]
extern crate alloc;
use alloc::vec::Vec;
use alloc::string::String;
use alloc::boxed::Box;
El segundo paso es imprescindible: designar un asignador global. Sin él, en cuanto tu código intente asignar, el enlazado falla porque no hay ninguna rutina a la que llamar. Un asignador global es un static que implementa el trait GlobalAlloc, cuyo contrato son dos operaciones sobre punteros crudos:
use core::alloc::{GlobalAlloc, Layout};
struct MiAsignador;
unsafe impl GlobalAlloc for MiAsignador {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
// pedir layout.size() bytes con alineacion layout.align()
// devolver el puntero al bloque, o null si no hay memoria
todo!()
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
// liberar el bloque que encabeza ptr, del mismo layout
todo!()
}
}
#[global_allocator]
static ASIGNADOR: MiAsignador = MiAsignador;
El Layout transporta el tamaño y la alineación de lo que se pide. La implementación es unsafe porque devolver un puntero mal alineado o un bloque de tamaño equivocado es comportamiento indefinido. En la práctica rara vez la escribes a mano: en embebido usas crates como embedded-alloc sobre una región estática de RAM, y en un no_std hospedado puedes envolver el asignador del sistema. Con el asignador en su sitio, todo vuelve a funcionar:
fn construir() -> Vec<i32> {
let mut v = Vec::new(); // ahora hay heap: Vec asigna sin problema
v.push(1);
v.push(2);
v
}
Un detalle práctico: alloc no instala un prelude propio, así que cada tipo se importa por su ruta —alloc::vec::Vec, alloc::string::String— y las macros se traen con use alloc::{vec, format};. Es más verboso que en std, pero también más explícito: deja a la vista exactamente qué del montículo consume cada módulo.
Qué recuperas y qué no
alloc te devuelve un arsenal completo de tipos del montículo, pero no la librería entera:
Punteros propietarios
Box para una asignación única, y los contadores de referencia Rc y Arc (alloc::rc y alloc::sync).
Colecciones que crecen
Vec, String, VecDeque, BTreeMap, BTreeSet, BinaryHeap, y las macros vec! y format!.
Prestado o propio
Cow y el trait ToOwned, para decidir en tiempo de ejecución entre tomar prestado y asignar una copia.
Lo que NO vuelve
HashMap, HashSet, ficheros, hilos y red: viven en std porque necesitan el SO, no solo el heap.
La ausencia más sonada es HashMap, que se queda en std por el hasher aleatorio sembrado por el SO que ya viste. Con alloc usas BTreeMap, o traes hashbrown con un hasher determinista. Un último detalle operativo: cuando una asignación falla —el asignador devuelve null—, alloc enruta el fallo a un manejador de error de asignación. Desde Rust 1.68 hay un comportamiento por defecto en estable (aborta el proceso), y personalizarlo con #[alloc_error_handler] sigue siendo una característica de nightly.
Conviene subrayar que estos tipos no son primos pobres de los de std: son exactamente los mismos. El Vec que obtienes vía alloc es idéntico byte a byte al que usarías con std, porque std no lo reimplementa: lo reexporta desde alloc. La misma capacidad amortizada, el mismo crecimiento geométrico, la misma semántica de Drop. Lo único que cambia entre un servidor y un microcontrolador es quién provee los bytes; el comportamiento de la colección es invariante.
Un asignador global, o uno por colección
#[global_allocator] instala un asignador para todo el proceso: cada Box y cada Vec acuden a él. Es lo que casi siempre quieres en no_std. Pero existe un segundo mecanismo, aún inestable, pensado para casos exigentes: el trait Allocator (la feature allocator_api), que permite que cada colección lleve su propio asignador como parámetro de tipo.
// Con la feature inestable allocator_api, el asignador viaja en el tipo:
// let v: Vec<u8, MiArena> = Vec::new_in(arena);
La diferencia conceptual es honda. GlobalAlloc responde a la pregunta “de donde sale la memoria del programa”, una sola vez y para todo. Allocator responde a “de donde sale la memoria de esta estructura”, tantas veces como quieras: una arena por petición, un pool por subsistema, memoria de un dispositivo concreto. En estable te bastará casi siempre el asignador global; conocer el otro eje te dice hasta dónde llega el control fino cuando de verdad lo necesitas.
La mayoría de los lenguajes empaquetan “el runtime” como una cosa indivisible: lo tomas entero o no tomas nada, y arrastras el recolector de basura, el asignador y la biblioteca de E/S juntos a todas partes. Rust hizo lo contrario, y alloc es la pieza que revela la jugada. Descompuso lo que otros llaman monolíticamente “la librería estándar” en tres capas ortogonales, cada una atada a una capacidad concreta del entorno: core es cálculo puro y no presupone nada; alloc añade memoria dinámica y presupone solo un asignador; std añade el mundo y presupone un sistema operativo completo. La ortogonalidad es la clave. No es una escalera donde para llegar al segundo peldaño hay que pisar el primero de forma inseparable: son ejes independientes que ensamblas según lo que tu destino puede sostener. Un microcontrolador con dieciséis kilobytes de RAM y un pequeño asignador de bloques corre core más alloc y tiene Vec y String de verdad, con la misma semántica exacta que en un servidor —porque Vec no cambia; lo único que cambia es quién le da los bytes—. Escribir una librería contra core más alloc significa que la misma línea de código, sin un solo #[cfg], funciona idéntica en la nube y en el metal desnudo. Esa es la portabilidad de Rust en su forma más profunda: no un runtime gordo que finge que todos los entornos son iguales, sino una factorización que te deja pagar, capa a capa, exactamente por lo que hay debajo.
alloc es el crate intermedio entre core y std: aporta los tipos del montículo —Box, Vec, String, Rc, Arc, Cow, BTreeMap, format!— sin exigir un SO, solo un asignador. Se activa con extern crate alloc; y requiere un #[global_allocator] que implemente GlobalAlloc (alloc y dealloc sobre punteros crudos, guiados por un Layout). HashMap no vuelve: vive en std. Las tres capas —core, core más alloc, y std— son ortogonales: eliges el piso que tu entorno sostiene, y un Vec es el mismo en todos ellos.
- Parte de un crate
no_std, añadeextern crate alloc;e intenta usarVecsin declarar asignador: lee el error de enlazado y explícalo. - Enlaza un asignador ya hecho (por ejemplo
embedded-alloco un envoltorio del sistema) y comprueba queVec::pushvuelve a funcionar. - Escribe la firma de
GlobalAllocde memoria: ¿qué transportaLayouty por quéallocydeallocsonunsafe? - Sustituye un
HashMappor unBTreeMapen códigono_stdconallocy razona qué garantía cambia (orden frente a dispersión). - Diseña la separación de capas de una librería tuya: ¿qué parte podría ser
corepuro, cuál necesitaríaallocy cuál obligaría astd?