Unsafe en Rust
Todas las garantías de las que ha hablado este curso (nada de referencias colgantes, nada de data races, nada de accesos fuera de límites) las hace cumplir el compilador. unsafe es la forma de decirle al compilador: “déjame hacer algo que tú no puedes comprobar que sea seguro; yo me hago responsable”.
Qué hace unsafe y qué no hace
Sección titulada «Qué hace unsafe y qué no hace»Un malentendido muy común es pensar que unsafe apaga por completo las comprobaciones de seguridad de Rust. No es así. Dentro de un bloque unsafe puedes hacer exactamente cinco cosas extra que, si no, están prohibidas:
- Desreferenciar un puntero crudo (raw pointer)
- Llamar a una función
unsafe - Leer o modificar una variable
staticmutable - Implementar un trait
unsafe - Acceder a los campos de una
union
Todo lo demás (las reglas de borrowing, la semántica de move, la comprobación de tipos) sigue aplicándose por completo dentro del código unsafe. Es un bisturí para cinco operaciones concretas, no un interruptor de apagado.
Punteros crudos
Sección titulada «Punteros crudos»Las referencias normales de Rust (& y &mut) siempre apuntan a datos válidos y bien alineados: eso forma parte de lo que garantiza el compilador. Un puntero crudo (*const T o *mut T) renuncia a esa garantía, y por eso leerlo requiere unsafe:
fn main() { let value = 5; let pointer = &value as *const i32;
unsafe { println!("{}", *pointer); }}Salida:
5Puedes crear un puntero crudo en código seguro. Solo desreferenciarlo (leer el valor al que apunta) necesita unsafe, porque el compilador ya no puede comprobar que el puntero sea válido.
Llamar a una función unsafe
Sección titulada «Llamar a una función unsafe»Algunas funciones están marcadas como unsafe porque tienen requisitos que el compilador no puede comprobar por ti. Al llamarlas, prometes que tú mismo verificaste esos requisitos:
unsafe fn dangerous() { println!("esta función confía en que comprobaste sus requisitos");}
fn main() { unsafe { dangerous(); }}Salida:
esta función confía en que comprobaste sus requisitosEnvolver unsafe en una función segura
Sección titulada «Envolver unsafe en una función segura»La biblioteca estándar tiene funciones unsafe reales que puedes llamar. Una de ellas es get_unchecked, de los slices. Funciona como numbers[i], pero se salta la comprobación de límites. Si i está fuera de rango, no provoca un panic: lee la memoria que haya en ese lugar, lo que se llama comportamiento indefinido (undefined behavior). Tu programa podría fallar, imprimir basura o parecer que funciona bien hasta que deja de hacerlo.
Así que la regla para llamarla es “solo con un índice que sabes que es válido”. El patrón habitual es comprobar esa regla tú mismo, dentro de una función normal y segura:
fn first_or_zero(numbers: &[i32]) -> i32 { if numbers.is_empty() { return 0; } // SAFETY: el slice no está vacío, así que el índice 0 está dentro de los límites. unsafe { *numbers.get_unchecked(0) }}
fn main() { println!("{}", first_or_zero(&[7, 8, 9])); println!("{}", first_or_zero(&[]));}Salida:
70Fíjate en lo que ve quien la llama: fn first_or_zero(numbers: &[i32]) -> i32. No hay unsafe en la firma, ni forma de llamarla mal, porque la función comprueba la regla antes de llegar siquiera al bloque unsafe. El unsafe se queda pequeño y escondido dentro.
Dos costumbres que siguen los desarrolladores de Rust con experiencia:
- Escribe un comentario
// SAFETY:encima de cada bloqueunsafe, explicando por qué se cumplen las reglas. Es para la próxima persona que lea el código, y para tu yo del futuro. La etiquetaSAFETY:se deja en inglés porque es la convención que buscan las herramientas, como Clippy. - Haz el bloque
unsafelo más pequeño posible, para que haya menos código que revisar dos veces.
Siendo sinceros, no escribirías esta función exacta en código real: numbers.first() hace lo mismo de forma segura, y el compilador es muy bueno eliminando las comprobaciones de límites que puede demostrar que no hacen falta. Pero el patrón (comprobar las reglas y luego llamar a unsafe dentro de una función segura) es exactamente como se construyen Vec, String y compañía. La página de FFI usa el mismo truco para envolver funciones de C.
Por qué existe unsafe
Sección titulada «Por qué existe unsafe»Si Rust gira en torno a la seguridad, ¿por qué permite esto? Porque hay cosas que de verdad es imposible demostrar que son seguras al compilar, aunque en la práctica lo sean: hablar directamente con el hardware, llamar a una biblioteca de C (consulta FFI) o construir ciertas estructuras de datos de alto rendimiento. unsafe permite que la biblioteca estándar (y, en raras ocasiones, tu propio código) implemente esas piezas de bajo nivel, mientras que todo lo que se construye encima sigue siendo Rust seguro normal, comprobado por completo.
El intercambio, con honestidad
Sección titulada «El intercambio, con honestidad»Escribir unsafe no significa que tu código ahora tenga errores. Significa que, en ese bloque concreto, el compilador ya no puede detectar por ti ciertos tipos de errores, y te toca a ti mantener las mismas garantías a mano. La mayoría de los desarrolladores de Rust con experiencia escriben muy poco código unsafe directamente; suele vivir en rincones pequeños y revisados con mucho cuidado, en lo más profundo de una biblioteca, envuelto en una API segura para que la use todo el mundo.
Práctica
Sección titulada «Práctica»1. Cambia un valor a través de un puntero crudo
Sección titulada «1. Cambia un valor a través de un puntero crudo»Crea let mut score = 10;, crea un puntero crudo *mut i32 hacia él y usa el puntero para sumarle 5. Después imprime score. Pista: crea el puntero con &mut score as *mut i32 y escribe *pointer += 5 dentro de un bloque unsafe.
Ver solución
fn main() { let mut score = 10; let pointer = &mut score as *mut i32; // crearlo es seguro
unsafe { *pointer += 5; // escribir a través de él no lo es }
println!("puntuación = {score}");}Salida:
puntuación = 15En la práctica esto es seguro porque score sigue viva y nada más la está usando mientras escribimos. Cumplir esas promesas es tu trabajo en cuanto usas unsafe.
2. Un get_or seguro
Sección titulada «2. Un get_or seguro»Escribe fn get_or(numbers: &[i32], index: usize, default: i32) -> i32. Devuelve el elemento en index, o default si el índice está fuera de rango. Usa get_unchecked para la lectura y asegúrate de que la función nunca se pueda llamar de una forma que rompa su regla. Pista: compara primero index < numbers.len() y llama a get_unchecked solo cuando sea cierto.
Ver solución
fn get_or(numbers: &[i32], index: usize, default: i32) -> i32 { if index < numbers.len() { // SAFETY: acabamos de comprobar que index está dentro de los límites. unsafe { *numbers.get_unchecked(index) } } else { default }}
fn main() { let numbers = [10, 20, 30]; println!("{}", get_or(&numbers, 1, -1)); println!("{}", get_or(&numbers, 5, -1));}Salida:
20-1El índice 5 nunca llega al bloque unsafe, así que se maneja de forma segura. Como comparación, la versión totalmente segura es numbers.get(index).copied().unwrap_or(default), y es lo que escribirías normalmente.
Resumen
Sección titulada «Resumen»unsafepermite exactamente cinco operaciones (sobre todo, punteros crudos y llamadas a funciones unsafe); todo lo demás se sigue comprobando por completo.- Un puntero crudo (
*const T/*mut T) se puede crear en código seguro; desreferenciarlo requiereunsafe. - Envuelve
unsafeen una función pequeña y segura que compruebe primero las reglas, y explica por qué es correcto en un comentario// SAFETY:. unsafeexiste para poder construir código de bajo nivel (acceso al hardware, interoperar con C), mientras el código que lo usa sigue siendo seguro.- Si estás empezando, rara vez tendrás que escribirlo: busca primero una abstracción segura.
Comprueba lo que aprendiste
Sección titulada «Comprueba lo que aprendiste»Comprueba lo aprendido
1. ¿Qué permite realmente un bloque unsafe?
2. ¿Puedes crear un puntero crudo en código seguro?
3. Si estás empezando, ¿con qué frecuencia deberías esperar escribir unsafe tú mismo?
4. ¿Escribir código unsafe significa que tu programa ahora tiene errores?
Puntuación: 0 / 4
Para leer más
Sección titulada «Para leer más»- Unsafe Rust — El libro de Rust (en inglés): los cinco “superpoderes de unsafe” en detalle, incluido cómo construir un
split_at_mutseguro sobre punteros crudos. - The Rustonomicon (en inglés): la guía oficial para escribir código unsafe correctamente. Es avanzada, pero vale mucho la pena leer el primer capítulo.
- La palabra clave
unsafe— documentación de la biblioteca estándar (en inglés): bloques, funciones y traits unsafe, además de la convención del comentario// SAFETY:. - Unsafety — The Rust Reference (en inglés): la lista exacta de operaciones que necesitan
unsafe. slice::get_unchecked— documentación de la biblioteca estándar (en inglés): una función unsafe real, con su sección “Safety” que detalla la regla que debes cumplir.