unsafe no apaga el borrow checker
El malentendido más caro sobre unsafe: creer que convierte Rust en C, que desactiva el borrow checker, el sistema de tipos o las vidas. No lo hace. unsafe es aditivo, no sustractivo: añade cinco poderes y no quita ninguna garantía. Quien aliasa no es la palabra unsafe, es el puntero crudo.
Aquí muere la carrera de muchos programadores que llegan de C: abren un bloque unsafe esperando que Rust “se aparte” y les deje hacer lo que quieran, y descubren, atónitos, que el borrow checker sigue ahí, gritándoles igual que fuera. El malentendido es total y es caro. unsafe no desactiva el borrow checker, no relaja el sistema de tipos, no suspende las reglas de vidas ni la semántica de movimiento, y no convierte Rust en C. Es una operación puramente aditiva: suma exactamente los cinco superpoderes de la lección anterior y no resta ni una sola garantía. Dentro de unsafe, dos &mut a lo mismo siguen siendo ilegales, un String sigue sin poder sumarse a un i32, y una referencia colgante se sigue rechazando. Lo que permite aliasar libremente no es la palabra unsafe: es el puntero crudo, que vive fuera del sistema de préstamos. Confundir ambas cosas es la raíz de casi todo el unsafe mal escrito del mundo.
- Refutar el mito de que
unsafeapaga el borrow checker, los tipos o las vidas. - Ver, en código que no compila, que las reglas de aliasing siguen vigentes dentro de
unsafe. - Entender que quien escapa del sistema de préstamos es el puntero crudo, no el bloque
unsafe. - Formular
unsafecomo operación aditiva: suma cinco poderes, no resta garantías.
El borrow checker sigue plenamente activo
Demostrémoslo con código. Este fragmento no compila, y el error es exactamente el mismo que verías fuera de cualquier bloque unsafe:
fn main() {
let mut v = vec![1, 2, 3];
unsafe {
let a = &mut v;
let b = &mut v; // ERROR: no puedes prestar `v` como mutable dos veces
a.push(4);
b.push(5);
}
}
El compilador rechaza el segundo &mut v dentro del unsafe con la misma firmeza que fuera. No hubo relajación alguna. El sistema de tipos también sigue intacto: dentro de unsafe, let x: i32 = String::new(); sigue siendo un error de tipos, y una referencia que sobreviva a su dueño sigue siendo rechazada por las vidas. unsafe no tocó nada de esa maquinaria.
Esto sorprende sobre todo a quien más experiencia trae de otros lenguajes de sistemas. En C no existe una frontera análoga: el compilador nunca comprobó el aliasing, así que no hay nada que “siga activo” al entrar en una zona peligrosa. En Rust, unsafe es una anotación dentro de un lenguaje que sigue comprobándolo todo, no un regreso a un lenguaje que no comprobaba nada. La diferencia es abismal y explica por qué el unsafe de Rust es órdenes de magnitud más seguro que C entero: opera sobre un sustrato que jamás deja de vigilar el resto.
El reflejo del principiante frustrado —“el borrow checker me molesta, lo meto en unsafe y listo”— no funciona, ni técnica ni conceptualmente. Técnicamente, el bloque unsafe no desbloquea el aliasing de referencias, así que tu código sigue sin compilar. Conceptualmente, aunque compilara, habrías prometido una seguridad que no verificaste. Si chocas con el borrow checker, la salida no es unsafe: es reestructurar, o —si de verdad necesitas aliasar— bajar deliberadamente a punteros crudos con todas sus consecuencias.
Quien aliasa es el puntero crudo, no unsafe
Entonces, ¿cómo se consigue el aliasing mutable que a veces se necesita? No abriendo un bloque unsafe, sino usando punteros crudos, que por diseño viven fuera del sistema de préstamos y por tanto no están sujetos a la regla de exclusividad:
fn main() {
let mut x = 10;
let p1 = &raw mut x; // puntero crudo: crear es seguro
let p2 = &raw mut x; // DOS punteros mut al mismo dato: PERMITIDO
unsafe {
*p1 += 1; // el bloque solo autoriza el desreferenciado
*p2 += 1;
}
println!("{x}");
}
Fíjate en el reparto de papeles. Los dos &raw mut x coexisten sin queja del borrow checker porque los punteros crudos no son referencias: no participan en el análisis de préstamos. El bloque unsafe no hizo nada por permitir ese aliasing; solo autorizó el acto de desreferenciar los punteros. La libertad vino del tipo *mut T; el bloque solo abrió el quinto centímetro del camino. Esta separación es la lección entera: el peligro no está en la palabra, está en el tipo de dato que elegiste usar.
flowchart LR subgraph Intacto[Garantias que unsafe NO toca] B[Borrow checker] T[Sistema de tipos] L[Vidas y movimiento] end subgraph Suma[Lo que unsafe anade] P[Cinco superpoderes] end Intacto --> R[Rust dentro de unsafe] Suma --> R style Intacto fill:#a6e3a1,color:#11111b style Suma fill:#f38ba8,color:#11111b style R fill:#89b4fa,color:#11111b
Aditivo, nunca sustractivo
La formulación precisa, la que conviene grabar: unsafe es un operador aditivo. Toma el lenguaje seguro completo —con todas sus comprobaciones vivas— y le añade cinco capacidades. No existe ninguna garantía que unsafe retire. Un bloque unsafe es Rust normal más cinco cosas, no Rust normal menos las comprobaciones.
Compruébalo con un caso mínimo: dentro de un bloque unsafe, el sistema de tipos sigue tan estricto como fuera.
unsafe {
let n: i32 = "hola"; // ERROR de tipos, igual que fuera de unsafe
let m: u8 = 300; // ERROR: 300 no cabe en u8, el compilador lo caza
}
Ni el error de tipos ni el de rango del literal desaparecen: unsafe ni siquiera los mira, porque no forman parte de los cinco superpoderes. Cada comprobación que existía fuera sigue existiendo, palabra por palabra, dentro del bloque.
De ahí se sigue una consecuencia liberadora para escribir buen unsafe: casi todo tu código dentro del bloque sigue siendo verificado por el compilador. Si tu bloque unsafe tiene veinte líneas y solo una desreferencia un puntero crudo, el compilador aún te protege en las otras diecinueve: te caza el error de tipos, el préstamo inválido, la vida demasiado corta. Por eso la buena práctica es hacer los bloques unsafe diminutos: cuanto menos código encierres, más trabajo sigue haciendo por ti el verificador y menos superficie queda librada a tu palabra.
El precio de escapar del sistema de préstamos
Que el puntero crudo te deje aliasar no es una laguna que explotar alegremente: es una puerta con peaje. En el momento en que sales del sistema de préstamos, pierdes también su protección, y el compromiso de mantener la unicidad de cada &mut —que el borrow checker sostenía por ti— pasa a ser tuyo. Considera dos vistas mutables a mitades de un buffer, construidas a mano:
fn dos_vistas(buf: &mut [u8]) {
let n = buf.len();
let p = buf.as_mut_ptr();
// SAFETY: las dos mitades no se solapan, asi que los &mut derivados son unicos.
let (a, b) = unsafe {
(
std::slice::from_raw_parts_mut(p, n / 2),
std::slice::from_raw_parts_mut(p.add(n / 2), n - n / 2),
)
};
a[0] = 1;
b[0] = 2; // legal: a y b jamas tocan la misma celda
}
El código es correcto, pero fíjate en lo que compraste con el puntero crudo y en lo que pagas por ello: el compilador ya no verifica que a y b no se solapen; lo garantizas tú con la aritmética p y p.add(n / 2), y lo documentas en el comentario // SAFETY:. Un error de un solo carácter aquí —n / 2 trocado por n / 3— crearía dos &mut solapados y con ellos comportamiento indefinido, y el borrow checker, al que sacaste de la ecuación, no diría una palabra. Escapar del sistema de préstamos siempre es esto: un poder a cambio de una responsabilidad que antes no tenías, y que el compilador ya no vigila.
Un truco diagnóstico para desmontar el mito por ti mismo: coge cualquier bloque unsafe y borra la palabra, dejando el cuerpo. Si el compilador solo protesta por los cinco superpoderes —una desreferencia cruda, una llamada unsafe— entonces el bloque estaba bien acotado. Si además se queja de tipos, préstamos o vidas, esas quejas también estaban ahí dentro del bloque: el unsafe nunca las silenció. Repetir este ejercicio un par de veces cura para siempre la ilusión de que unsafe apaga el compilador.
De aquí sale una consecuencia estratégica: como el borrow checker sigue vivo, escribir unsafe idiomático consiste en dejar que el compilador haga todo lo que pueda y reservar tu garantía manual para el mínimo irreducible. Los mejores bloques unsafe son aquellos en que casi todo el cuerpo se sigue verificando y solo una línea —la desreferencia, la llamada— descansa sobre tu palabra.
Si pudiéramos rebautizar unsafe, media confusión del ecosistema se evaporaría. La palabra sugiere “código inseguro”, un territorio donde las reglas se suspenden y todo vale, y esa sugerencia es falsa en cada uno de sus términos. Lo que unsafe significa de verdad es “aquí la seguridad no la comprueba la máquina; la avala un humano”. El borrow checker no se apaga, el sistema de tipos no se relaja, las vidas no se suspenden: el edificio entero de garantías sigue en pie, operando a pleno rendimiento sobre cada línea del bloque. Lo único que cambia es que se te concede ejercer cinco operaciones que la máquina no sabe verificar, y a cambio te haces responsable de que sigan siendo seguras. El malentendido de que “unsafe es hacer C dentro de Rust” es doblemente pernicioso. Es un error técnico, porque quien intenta aliasar dos referencias mutables dentro de un bloque descubre que no compila: el aliasing no lo desbloquea la palabra, lo desbloquea el puntero crudo, un tipo de dato que deliberadamente vive fuera del sistema de préstamos. Y es un error moral, porque quien mete un choque del borrow checker en unsafe creyendo que lo silencia, en el caso de que por casualidad compile, ha firmado una garantía que no posee: ha convertido una limitación de su diseño en una promesa falsa al optimizador. La consecuencia de fondo es que unsafe no te saca del rigor de Rust, te sumerge más en él: dentro de un bloque debes razonar con más cuidado que fuera, porque ahora sostienes a mano exactamente las invariantes que el compilador sostenía por ti, mientras él sigue vigilando todo lo demás. Escribir buen unsafe no es relajarse; es aceptar que el compilador te cubre las espaldas en el noventa y nueve por ciento del bloque y que ese uno por ciento restante lo firmas tú, con nombre y apellidos.
unsafe no apaga el borrow checker, ni el sistema de tipos, ni las vidas, ni convierte Rust en C. Es aditivo: suma cinco superpoderes y no resta ninguna garantía. Dos &mut a lo mismo siguen prohibidos dentro de unsafe; un error de tipos sigue siendo error. Lo que permite aliasar libremente es el puntero crudo —*mut T, o &raw mut—, que vive fuera del sistema de préstamos; el bloque solo autoriza a desreferenciarlo. Envolver un error del borrow checker en unsafe no lo arregla: no compila, y si compilara, sería una promesa falsa. Haz los bloques diminutos para que el verificador siga cubriendo todo lo demás.
- Copia el ejemplo de los dos
&mut vdentro deunsafey confirma que no compila. Compara el mensaje con el que da fuera deunsafe: ¿son distintos? - Reescribe ese aliasing con dos
&raw muty demuestra que ahora sí compila. Explica qué hizo el bloqueunsafey qué hizo el puntero crudo. - Dentro de un bloque
unsafe, intenta asignar unStringa una variablei32y comprueba que el sistema de tipos sigue vivo. - Escribe un bloque
unsafede quince líneas con una sola desreferencia y argumenta cuántas líneas sigue verificando el compilador y por qué conviene achicar el bloque. - Redacta, con tus palabras, por qué “meter el error del borrow checker en
unsafe” es a la vez un error técnico y un error moral.