`UnsafeCell<T>`: la primitiva sobre la que todo se construye
`UnsafeCell<T>` es la única puerta legal de Rust para mutar un dato a través de una referencia compartida: el único tipo que el compilador exime de la suposición de no-aliasing que aplica a todo `&`. `Cell`, `RefCell`, `Mutex`, `RwLock` y los atómicos bottom-out en él. Su `get` devuelve un `*mut T` crudo cuyo uso es `unsafe`: por dentro exige unsafe porque tú sostienes el invariante; por fuera es seguro porque el envoltorio lo demuestra.
Toda la mutabilidad interior que has visto —Cell moviendo valores, RefCell contando préstamos, Mutex cerrando con llave— descansa sobre un único tipo primitivo: UnsafeCell<T>. No es una abstracción más de la biblioteca; es un elemento del lenguaje, conocido por el compilador, con un superpoder que ningún otro tipo tiene: es el único a través del cual está permitido mutar un dato mientras existen referencias compartidas & a él. Todo lo demás en Rust —cada &T— viene con la promesa inquebrantable de que su dato no cambiará bajo los pies del lector, y el optimizador cuenta con ello. UnsafeCell es la excepción declarada, la costura donde esa promesa se suspende a propósito y de forma acotada. Por dentro exige unsafe; por fuera, envuelto con disciplina, es perfectamente seguro.
- Explicar por qué mutar tras un
&es UB salvo a través deUnsafeCell. - Usar
UnsafeCell::newyget, y entender por quégetdevuelve un*mut T. - Ver
UnsafeCellcomo el fondo común deCell,RefCell,Mutexy los atómicos. - Articular qué significa “unsafe por dentro, seguro por fuera” como definición de abstracción segura.
La única excepción a la promesa del &
En Rust, un &T no es solo “un puntero de lectura”: es una garantía que el compilador explota para optimizar. Al ver un &T, el optimizador asume que ese dato no cambiará mientras la referencia viva —lo marca, en términos de LLVM, como noalias y readonly—, y sobre esa base reordena, cachea en registros y elimina relecturas. Mutar un dato detrás de un &T corriente rompería esa suposición y sería comportamiento indefinido: no un error, algo peor, un programa cuyo significado el compilador ya no garantiza.
UnsafeCell<T> es la única forma de decirle al compilador “aquí no apliques esa suposición”. Es un lang item: el compilador lo reconoce por su nombre y trata de modo especial cualquier dato que viva dentro de uno, absteniéndose de marcar noalias el interior. Por eso —y solo por eso— mutar a través de &UnsafeCell<T> es legal. Cualquier tipo que ofrezca mutabilidad interior debe, por ley del lenguaje, tener un UnsafeCell en su núcleo; no hay otra manera correcta de hacerlo.
Su interfaz cruda: get devuelve un *mut T
UnsafeCell no finge ser seguro. Su método central, get, toma &self y devuelve un puntero crudo mutable al interior:
use std::cell::UnsafeCell;
let celda = UnsafeCell::new(10);
let p: *mut i32 = celda.get(); // &self -> *mut i32, esto es seguro...
unsafe {
*p = 20; // ...pero desreferenciar y escribir es UNSAFE
}
assert_eq!(celda.into_inner(), 20);
Obtener el puntero es seguro; usarlo para leer o escribir requiere un bloque unsafe, porque en ese punto eres tú quien debe garantizar el invariante que el compilador ya no vigila: que no haya dos accesos en conflicto sobre esa dirección a la vez. UnsafeCell no comprueba nada, no cuenta nada, no cierra nada. Es materia prima: te entrega el permiso para mutar tras un & y te traspasa, entera, la responsabilidad de hacerlo bien.
El fondo común de todo
Aquí se cierra el nivel. Los tipos cómodos y seguros que has usado no son magia ni primitivas independientes: son UnsafeCell envuelto en una disciplina que demuestra el invariante que UnsafeCell deja en tus manos.
// Esencia (simplificada) de las abstracciones del nivel:
pub struct Cell<T> { value: UnsafeCell<T> } // disciplina: no prestar refs
pub struct RefCell<T> { borrow: Cell<isize>, value: UnsafeCell<T> } // disciplina: contar
// Mutex<T>, RwLock<T>, AtomicUsize... todos: un UnsafeCell + un mecanismo de exclusion
Cada uno resuelve, a su manera, la misma pregunta —¿cómo garantizo que las mutaciones no se solapen?— y cada respuesta es una disciplina distinta encima del mismo núcleo inseguro. Cell no presta referencias, así que no hay conflicto posible. RefCell cuenta los préstamos y aborta antes del conflicto. Mutex serializa con un cerrojo. Los atómicos usan instrucciones del procesador que son indivisibles. Distinta prueba, mismo fundamento.
flowchart TB unsafecell[UnsafeCell de T la unica puerta legal] --> cell[Cell no presta refs] unsafecell --> refcell[RefCell cuenta prestamos] unsafecell --> mutex[Mutex y RwLock cierran con llave] unsafecell --> atomic[Atomicos instrucciones indivisibles] cell --> seguro[Abstracciones seguras] refcell --> seguro mutex --> seguro atomic --> seguro seguro --> usuario[Codigo de usuario sin ningun unsafe] style unsafecell fill:#89b4fa,color:#11111b style seguro fill:#a6e3a1,color:#11111b style usuario fill:#cba6f7,color:#11111b
Construir una Cell con tus propias manos
Nada aclara tanto la relación entre el núcleo inseguro y la cáscara segura como reconstruir Cell desde cero. Bastan un UnsafeCell y la disciplina de no prestar jamás el interior:
use std::cell::UnsafeCell;
struct MiCell<T> {
valor: UnsafeCell<T>,
}
impl<T: Copy> MiCell<T> {
fn new(v: T) -> Self {
MiCell { valor: UnsafeCell::new(v) }
}
fn get(&self) -> T {
// SEGURO: devolvemos una COPIA, nunca un &T al interior,
// asi que ninguna lectura puede solaparse con una escritura.
unsafe { *self.valor.get() }
}
fn set(&self, v: T) {
// SEGURO: MiCell es !Sync, luego ningun otro hilo escribe a la vez,
// y como get copia, no hay un &T vivo que esto invalide.
unsafe { *self.valor.get() = v; }
}
}
Los dos unsafe marcan las costuras donde el compilador se retira; los comentarios son la prueba que los justifica. La disciplina —copiar en get, no prestar nunca— es lo único que separa esta miniatura correcta de una carrera de datos. Por fuera, MiCell no expone ni un unsafe: quien la usa vive en tierra segura, sostenido por un teorema de cuatro líneas que alguien demostró una vez. Reconstruir RefCell sería el mismo ejercicio con una disciplina más rica: añadir un contador y comprobarlo antes de cada préstamo.
UnsafeCell<T> es !Sync: por defecto no se comparte entre hilos, aunque T sí sea compartible. Es coherente con todo lo visto —mutar tras un & sin sincronizar entre hilos es una carrera de datos—. Los envoltorios seguros para varios hilos, como Mutex, reactivan Sync explícitamente con un unsafe impl Sync, y esa firma es una promesa del autor: “he añadido la exclusión que hace segura la compartición”. Ese unsafe impl es otra costura auditada, hermana de la de get.
Llegas al fondo del edificio y encuentras, en su cimiento, un bloque unsafe. Es tentador leerlo como una contradicción —¿no era Rust el lenguaje seguro?—, pero es exactamente lo contrario: es la revelación de qué significa “seguro” de verdad. La seguridad de Rust nunca fue la inexistencia de operaciones peligrosas; ninguna abstracción interesante puede construirse sin ellas, porque mutar memoria compartida, al final, es peligroso a nivel de máquina. La seguridad de Rust es el confinamiento de ese peligro a un número diminuto de costuras explícitas, auditables y demostradas, sobre las cuales se erige una superficie inmensa de código que no necesita saber que existen. UnsafeCell es la costura primordial de la mutabilidad interior: el punto exacto donde el compilador dice “hasta aquí llega mi demostración; a partir de aquí, el invariante lo sostienes tú”, y donde el autor de Cell o de Mutex responde con una prueba —informal pero rigurosa— de que su disciplina lo sostiene sin fisuras. Lo que ocurre entonces es una transferencia de la carga de la prueba, y luego su devolución: el compilador demuestra casi todo automáticamente; en las poquísimas fronteras donde su lógica no alcanza, un humano completa la demostración dentro de un unsafe; y el resultado —si la prueba es correcta— vuelve a ser código totalmente seguro para todos los que lo usan por encima, que jamás escriben unsafe ni necesitan hacerlo. Un RefCell es seguro no porque no contenga peligro, sino porque envuelve su peligro en un contador que hace imposible el conflicto; un Mutex, en un cerrojo. La palabra unsafe no marca lo inseguro: marca lo no verificado por el compilador, lo que queda a cargo de una prueba humana. Y esa es la arquitectura profunda de todo el lenguaje, visible aquí en miniatura: un núcleo minúsculo de confianza cuidadosamente delimitada, sobre el que descansa un océano de código donde la seguridad es automática. Interioriza esto y unsafe deja de asustar y deja también de seducir: no es un permiso para hacer trampas ni una mancha en la pureza, sino la herramienta precisa con la que se extiende la frontera de lo seguro, un teorema demostrado a mano cada vez. UnsafeCell es donde ese teorema, para la mutabilidad interior, se enuncia por primera vez; todo lo demás del nivel es un corolario suyo.
UnsafeCell<T> es la primitiva del lenguaje para la mutabilidad interior: el único tipo que el compilador exime de la suposición de no-aliasing que aplica a todo &, y por tanto la única vía legal para mutar tras una referencia compartida. Su get devuelve un *mut T cuyo uso exige unsafe, porque el invariante de no-conflicto pasa a tus manos. Cell, RefCell, Mutex, RwLock y los atómicos lo envuelven, cada uno con una disciplina distinta que demuestra ese invariante: unsafe por dentro, seguro por fuera. Es !Sync; los envoltorios multi-hilo reactivan Sync con un unsafe impl auditado. La seguridad de Rust no es la ausencia de unsafe, sino su confinamiento a costuras demostradas.
- Crea un
UnsafeCell::new(10), obtén su*mut i32congety escribe a través de él en un bloqueunsafe. Explica qué línea es segura y cuál no, y por qué. - Investiga por qué escribir a través de un
&i32normal (con un*mutobtenido por conversión) es comportamiento indefinido, y por qué a través de&UnsafeCell<i32>no lo es. - Dibuja el árbol de dependencias: parte de
UnsafeCelly llega aCell,RefCellyMutex, anotando qué disciplina añade cada uno. - Explica por qué
UnsafeCell<T>es!Syncy qué significa queMutex<T>hagaunsafe impl Sync. - Redacta en tres frases qué quiere decir “la seguridad de Rust es el confinamiento del peligro, no su ausencia”, usando
UnsafeCellcomo ejemplo.