Unsafe Rust
Todo el código que hemos discutido hasta ahora ha tenido las garantías de seguridad de memoria de Rust aplicadas en tiempo de compilación. Sin embargo, Rust tiene un segundo lenguaje oculto dentro de él que no hace cumplir estas garantías de seguridad de memoria: se llama unsafe Rust y funciona como Rust regular, pero nos da superpoderes adicionales.
Unsafe Rust existe porque, por naturaleza, el análisis estático es conservador. Cuando el compilador intenta determinar si el código cumple o no con las garantías, es mejor que rechace algunos programas válidos que aceptar algunos programas no válidos. Aunque el código podría estar bien, si el compilador de Rust no tiene suficiente información para estar seguro, rechazará el código. En estos casos, puede usar código inseguro para decirle al compilador: "Confía en mí, sé lo que estoy haciendo". Sin embargo, debes tener cuidado, ya que el uso de Unsafe Rust conlleva riesgos: si usas código inseguro de manera incorrecta, pueden ocurrir problemas debido a la inseguridad de la memoria, como la desreferenciación de puntero nulo.
Otra razón por la que Rust tiene un alter ego inseguro es que el hardware informático subyacente es inherentemente inseguro. Si Rust no le permitiera realizar operaciones inseguras, no podría realizar ciertas tareas. Rust necesita permitirle realizar programación de sistemas de bajo nivel, como interactuar directamente con el sistema operativo o incluso escribir su propio sistema operativo. Trabajar con programación de sistemas de bajo nivel es uno de los objetivos del lenguaje. Veamos qué podemos hacer con Rust inseguro y cómo hacerlo.
Superpoderes Unsafe
Para cambiar a Unsafe Rust, use la palabra clave unsafe
y luego comience un
nuevo bloque que contenga el código inseguro. Puede tomar cinco acciones en
Rust inseguro que no puede en Rust seguro, que llamamos superpoderes
Unsafe. Esos superpoderes incluyen la capacidad de:
- Desreferenciar un puntero crudo
- Llamar a una función o método inseguro
- Acceder o modificar una variable estática mutable
- Implementar un trait inseguro
- Acceder a los campos de un
union
Es importante entender que unsafe
no desactiva el borrow checker ni
deshabilita ninguna otra de las comprobaciones de seguridad de Rust: si usa una
referencia en código inseguro, aún se verificará. La palabra clave unsafe
solo le da acceso a estas cinco funciones que luego no son verificadas por el
compilador para la seguridad de la memoria. Aún obtendrá cierto grado de
seguridad dentro de un bloque inseguro.
Además, unsafe
no significa que el código dentro del bloque sea
necesariamente peligroso o que definitivamente tendrá problemas de seguridad de
memoria: la intención es que, como programador, se asegurará de que el código
dentro de un bloque unsafe
acceda a la memoria de una manera válida.
Las personas son falibles y pueden cometer errores, pero al requerir que estas
cinco operaciones inseguras estén dentro de bloques anotados con unsafe
,
sabrá que cualquier error relacionado con la seguridad de la memoria debe estar
dentro de un bloque unsafe
. Mantenga los bloques unsafe
pequeños; lo
agradecerá más tarde cuando investigue bugs de memoria.
Para aislar el código inseguro tanto como sea posible, es mejor encerrar el
código inseguro dentro de una abstracción segura y proporcionar una API segura,
que discutiremos más adelante en el capítulo cuando examinemos las funciones y
métodos inseguros. Partes de la biblioteca estándar se implementan como
abstracciones seguras sobre código inseguro que ha sido auditado. Envolver el
código inseguro en una abstracción segura evita que los usos de unsafe
se
filtren en todos los lugares que usted o sus usuarios puedan querer usar la
funcionalidad implementada con código unsafe
, porque usar una abstracción
segura es seguro.
Veamos cada uno de los cinco superpoderes unsafe a su vez. También veremos algunas abstracciones que proporcionan una interfaz segura al código inseguro.
Desreferenciación de un puntero crudo
En el Capítulo 4, en la sección Referencias y punteros
Unsafe Rust tiene dos nuevos tipos llamados punteros crudos que son similares
a las referencias. Al igual que con las referencias, los punteros crudos pueden
ser inmutables o mutables y
se escriben como *const T
y *mut T
, respectivamente. El asterisco no es el
operador de desreferencia; es parte del nombre del tipo. En el contexto de los
punteros crudos, inmutable significa que el puntero no se puede asignar
directamente después de ser desreferenciado.
A Diferencia de las referencias y los smart pointers, los punteros crudos:
- Son permitidos ignorar las reglas de borrowing al tener tanto punteros inmutables como mutables o múltiples punteros mutables al mismo lugar
- No se garantiza que apunten a una memoria válida
- Se les permite ser nulos
- No implementan ninguna limpieza automática
Al optar por no hacer que Rust haga cumplir estas garantías, puede renunciar a la seguridad garantizada a cambio de un mayor rendimiento o la capacidad de interactuar con otro lenguaje o hardware donde las garantías de Rust no se aplican.
El Listing 19-1 muestra cómo crear un puntero crudo inmutable y mutable a partir de referencias.
fn main() { let mut num = 5; let r1 = &num as *const i32; let r2 = &mut num as *mut i32; }
Listing 19-1: Creando punteros crudos a partir de referencias
Observa que no incluimos la palabra clave unsafe
en este código. Podemos
crear punteros crudos en código seguro; simplemente no podemos desreferenciar
punteros crudos fuera de un bloque unsafe
, como verás en un momento.
Hemos creado punteros crudos utilizando as
para convertir una referencia
inmutable y una mutable en sus tipos de puntero crudo correspondientes. Como
los creamos directamente a partir de referencias garantizadas como válidas,
sabemos que estos punteros crudos particulares son válidos, pero no podemos
hacer esa suposición sobre cualquier puntero crudo.
Para demostrar esto, a continuación crearemos un puntero crudo cuya validez no podemos estar tan seguros. El Listado 19-2 muestra cómo crear un puntero crudo a una ubicación arbitraria en la memoria. Intentar usar memoria arbitraria es indefinido: puede haber datos en esa dirección o no, el compilador puede optimizar el código para que no haya acceso a la memoria, o el programa puede generar un error con un fallo de segmentación. Por lo general, no hay una buena razón para escribir código como este, pero es posible.
fn main() { let address = 0x012345usize; let r = address as *const i32; }
Listing 19-2: Creando un puntero crudo a una dirección de memoria arbitraria
Recuerda que podemos crear punteros crudos en código seguro, pero no podemos
desreferenciar punteros crudos y leer la memoria a la que apuntan fuera de un
bloque unsafe
.
fn main() { let mut num = 5; let r1 = &num as *const i32; let r2 = &mut num as *mut i32; unsafe { println!("r1 is: {}", *r1); println!("r2 is: {}", *r2); } }
Listing 19-3: Desreferenciando punteros crudos dentro de
un bloque unsafe
Crear un puntero no causa daño; solo cuando intentamos acceder al valor al que apunta que podríamos terminar tratando con un valor no válido.
También ten en cuenta que en los Listados 19-1 y 19-3, creamos *const i32
y
*mut i32
punteros crudos que apuntaban a la misma ubicación de memoria, donde
se almacena num
. Si en su lugar intentáramos crear una referencia inmutable y
mutable a num
, el código no se compilaría porque las reglas de ownership de
Rust no permiten una referencia mutable al mismo tiempo que cualquier referencia
inmutable. Con punteros crudos, podemos crear un puntero mutable y un puntero
inmutable a la misma ubicación y cambiar los datos a través del puntero mutable,
potencialmente creando una carrera de datos. ¡Ten cuidado!
Con todos estos peligros, ¿por qué usarías punteros crudos? Un caso de uso importante es cuando se interactúa con código C, como verás en la siguiente sección, “Llamando a una función o método inseguro”. Otro caso es cuando se construyen abstracciones seguras que el borrow checker no entiende. Presentaremos funciones inseguras y luego veremos un ejemplo de una abstracción segura que usa código inseguro.
Llamando a una funcion o metodo inseguro
El segundo tipo de operación que solo se puede realizar en un bloque unsafe es
llamar a una función o método inseguro. Podemos crear funciones inseguras y
métodos inseguros que se ven exactamente como funciones y métodos regulares,
pero tienen un unsafe
adicional antes del resto de la definición. La palabra
clave unsafe
en este contexto indica que la función tiene requisitos que
debemos cumplir cuando llamamos a esta función porque Rust no puede garantizar
que hayamos cumplido con estos requisitos. Al llamar a una función insegura
dentro de un bloque unsafe
, estamos diciendo que hemos leído la documentación
de esta función y asumimos la responsabilidad de cumplir con los contratos de
la función.
Aquí hay un ejemplo de una función insegura llamada dangerous
que no hace
nada en su cuerpo:
fn main() { unsafe fn dangerous() {} unsafe { dangerous(); } }
Debemos llamar a la función dangerous
dentro de un bloque unsafe
separado.
Si intentamos llamar a esta función sin un bloque unsafe
, obtendremos un
error:
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function `dangerous` is unsafe and requires unsafe function or block
--> src/main.rs:4:5
|
4 | dangerous();
| ^^^^^^^^^^^ call to unsafe function
|
= note: consult the function's documentation for information on how to avoid undefined behavior
For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
Con el bloque unsafe
, le estamos indicando a Rust que hemos leído la
documentación de la función, entendemos cómo usarla correctamente y hemos
verificado que estamos cumpliendo con el contrato de la función.
Los cuerpos de las funciones unsafe
son similares a los bloques unsafe
,
por lo que para realizar otras operaciones unsafe
dentro de una función
unsafe
, no necesitamos agregar otro bloque unsafe
.
Creando una abstracción segura sobre código inseguro
Solo porque una función contiene código inseguro no significa que debamos
marcar toda la función como insegura. De hecho, envolver el código inseguro en
una función segura es una abstracción común. Como ejemplo, estudiemos la
función split_at_mut
de la biblioteca estándar, que requiere algo de código
inseguro. Exploraremos cómo podríamos implementarlo. Este método seguro está
definido en slices mutables: toma un slice y lo divide en dos al dividir
el slice en el índice dado como argumento. El Listado 19-4 muestra cómo usar
split_at_mut
.
fn main() { let mut v = vec![1, 2, 3, 4, 5, 6]; let r = &mut v[..]; let (a, b) = r.split_at_mut(3); assert_eq!(a, &mut [1, 2, 3]); assert_eq!(b, &mut [4, 5, 6]); }
Listing 19-4: Usando la función segura
split_at_mut
No podemos implementar esta función utilizando solo Rust seguro. Un intento
podría ser algo como el Listado 19-5, que no se compilará. Para simplificar,
implementaremos split_at_mut
como una función en lugar de un método y solo
para slices de valores i32
en lugar de para un tipo genérico T
.
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
assert!(mid <= len);
(&mut values[..mid], &mut values[mid..])
}
fn main() {
let mut vector = vec![1, 2, 3, 4, 5, 6];
let (left, right) = split_at_mut(&mut vector, 3);
}
Listing 19-5: Un intento de implementación de
split_at_mut
usando solo Rust seguro
Esta función primero obtiene la longitud total del slice. Luego verifica si el índice dado como parámetro está dentro del slice al verificar si es menor o igual a la longitud. La aserción significa que si pasamos un índice que es mayor que la longitud para dividir el slice, la función entrará en panic antes de intentar usar ese índice.
Luego, devolvemos dos slices mutables en una tupla: uno desde el inicio del
slice original hasta el índice mid
y otro desde mid
hasta el final del
slice.
Cuando intentamos compilar el código en el Listado 19-5, obtendremos un error:
$ cargo run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
--> src/main.rs:6:31
|
1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
| - let's call the lifetime of this reference `'1`
...
6 | (&mut values[..mid], &mut values[mid..])
| --------------------------^^^^^^--------
| | | |
| | | second mutable borrow occurs here
| | first mutable borrow occurs here
| returning this value requires that `*values` is borrowed for `'1`
For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error
El borrow checker de Rust no puede entender que estamos tomando prestado diferentes partes del slice; solo sabe que estamos tomando prestado el mismo slice dos veces. Tomar prestadas diferentes partes de un slice es fundamentalmente correcto porque los dos slices no se superponen, pero Rust no es lo suficientemente inteligente como para saber esto. Cuando sabemos que el código está bien, pero Rust no lo sabe, es hora de recurrir al código inseguro.
El Listado 19-6 muestra cómo usar un bloque unsafe
, un puntero sin procesar
y algunas llamadas a funciones inseguras para hacer que la implementación de
split_at_mut
funcione.
use std::slice; fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { let len = values.len(); let ptr = values.as_mut_ptr(); assert!(mid <= len); unsafe { ( slice::from_raw_parts_mut(ptr, mid), slice::from_raw_parts_mut(ptr.add(mid), len - mid), ) } } fn main() { let mut vector = vec![1, 2, 3, 4, 5, 6]; let (left, right) = split_at_mut(&mut vector, 3); }
Listing 19-6: Usando código inseguro en la implementación
de la función split_at_mut
Recordemos la sección “The Slice Type” del
Capítulo 4 que los slices son un puntero a algunos datos y la longitud del
slice. Usamos el método len
para obtener la longitud del slice y el método
as_mut_ptr
para acceder al puntero sin procesar de un slice. En este caso,
porque tenemos un slice mutable a valores i32
, as_mut_ptr
devuelve un
puntero sin procesar con el tipo *mut i32
, que hemos almacenado en la
variable ptr
.
Mantenemos la afirmación de que el índice mid
está dentro del slice. Luego
llegamos al código inseguro: la función slice::from_raw_parts_mut
toma un
puntero sin procesar y una longitud, y crea un slice. Usamos esta función para
crear un slice que comienza desde ptr
y es mid
elementos de largo. Luego
llamamos al método add
en ptr
con mid
como argumento para obtener un
puntero sin procesar que comienza en mid
, y creamos un slice usando ese
puntero y el número restante de elementos después de mid
como la longitud.
La función slice::from_raw_parts_mut
es insegura porque toma un puntero sin
procesar y debe confiar en que este puntero es válido. El método add
en
punteros sin procesar también es inseguro porque debe confiar en que la
ubicación del desplazamiento también es un puntero válido. Por lo tanto,
tuvimos que poner un bloque unsafe
alrededor de nuestras llamadas a
slice::from_raw_parts_mut
y add
para poder llamarlas. Al mirar el código y
al agregar la afirmación de que mid
debe ser menor o igual a len
, podemos
decir que todos los punteros sin procesar utilizados dentro del bloque
unsafe
serán punteros válidos a datos dentro del slice. Este es un uso
aceptable y apropiado de unsafe
.
Tenga en cuenta que no necesitamos marcar la función resultante split_at_mut
como unsafe
, y podemos llamar a esta función desde Rust seguro. Hemos creado
una abstracción segura para el código inseguro con una implementación de la
función que usa código unsafe
de manera segura, porque crea solo punteros
válidos a partir de los datos a los que esta función tiene acceso.
Por el contrario, el uso de slice::from_raw_parts_mut
en el Listado 19-7
probablemente se bloqueará cuando se use el slice. Este código toma una
ubicación de memoria arbitraria y crea un slice de 10,000 elementos.
fn main() { use std::slice; let address = 0x01234usize; let r = address as *mut i32; let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) }; }
Listing 19-7: Creando un slice a partir de una ubicación de memory arbitraria
No somos propietarios de la memoria en esta ubicación arbitraria, y no hay
garantía de que el slice que este código crea contenga valores i32
válidos.
Intentar usar values
como si fuera un slice válido da como resultado un
comportamiento indefinido.
Usando funciones extern
para llamar código externo
A veces, tu código en Rust necesita interactuar con código escrito en otro
lenguaje. Para esto, Rust tiene la palabra clave extern
que facilita la
creación y el uso de una Foreign Function Interface (FFI). Una FFI es una
forma para que un lenguaje de programación defina funciones y permita que un
lenguaje de programación diferente (extranjero) llame a esas funciones.
El Listado 19-8 demuestra cómo configurar una integración con la función abs
de la biblioteca estándar de C. Las funciones declaradas dentro de bloques
extern
siempre son inseguras de llamar desde el código Rust. La razón es que
otros lenguajes no hacen cumplir las reglas y garantías de Rust, y Rust no
puede verificarlas, por lo que la responsabilidad recae en el programador para
garantizar la seguridad.
Filename: src/main.rs
extern "C" { fn abs(input: i32) -> i32; } fn main() { unsafe { println!("Absolute value of -3 according to C: {}", abs(-3)); } }
Listing 19-8: Declarando y llamando a una función extern
definida en otro lenguaje
Dentro del bloque extern "C"
en el Listado 19-8, enumeramos los nombres y
las firmas de las funciones externas que queremos llamar. El nombre y la firma
de la función abs
se definen en el estándar C y son parte de la biblioteca
estándar de C. La firma de la función abs
es int abs(int)
, lo que significa
que toma un argumento int
y devuelve un int
. La función abs
devuelve el
valor absoluto de su argumento.
Llamando a funciones Rust desde otros lenguajes
También podemos usar
extern
para crear una interfaz que permita que otros lenguajes llamen funciones Rust. En lugar de crear un bloqueextern
, podemos agregar la palabra claveextern
y especificar la ABI a usar justo antes de la palabra clavefn
para la función relevante. También necesitamos agregar una anotación#[no_mangle]
para decirle al compilador de Rust que no cambie el nombre de esta función. Mangling es cuando un compilador cambia el nombre que le hemos dado a una función a un nombre diferente que contiene más información para otras partes del proceso de compilación para consumir, , pero es menos legible para los humanos. Cada compilador de lenguaje de programación mangla los nombres de manera ligeramente diferente, por lo que para que una función Rust sea nombrable por otros lenguajes, debemos deshabilitar el mangling del compilador de Rust.En el siguiente ejemplo, hacemos que la función
call_from_c
sea accesible desde el código C, después de que se compile a una biblioteca compartida y se vincule desde C:#![allow(unused)] fn main() { #[no_mangle] pub extern "C" fn call_from_c() { println!("Just called a Rust function from C!"); } }
Este uso de
extern
no requiereunsafe
.
Acceder o modificar una variable estática mutable
En este libro, aún no hemos hablado de variables globales, las cuales Rust admite, pero pueden ser problemáticas con las reglas de ownership de Rust. Si dos hilos acceden a la misma variable global mutable, puede causar una condición de carrera.
En Rust, las variables globales son llamadas variables static. El Listado 19-9 muestra un ejemplo de declaración y uso de una variable static con un string slice como valor.
Filename: src/main.rs
static HELLO_WORLD: &str = "Hello, world!"; fn main() { println!("name is: {HELLO_WORLD}"); }
Listing 19-9: Definición y uso de una variable static inmutable
Las static variables son similares a las constantes, que discutimos en la
sección "Diferencias entre variables y
constantes" en el Capítulo 3. Los
nombres de las variables static están en SCREAMING_SNAKE_CASE
por convención.
Las variables static solo pueden almacenar referencias con el lifetime
'static
, lo que significa que el compilador de Rust puede calcular el
lifetime y no estamos obligados a anotarlo explícitamente. Acceder a una
variable static inmutable es seguro.
Una diferencia sutil entre constantes y variables static inmutables es que los
valores en una variable static tienen una dirección fija en la memoria. Usar el
valor siempre accederá a los mismos datos. Las constantes, por otro lado,
pueden duplicar sus datos cada vez que se usan. Otra diferencia es que las
variables static pueden ser mutables. Acceder y modificar variables static
mutables es inseguro. El Listado 19-10 muestra cómo declarar, acceder y
modificar una variable static mutable llamada COUNTER
.
Filename: src/main.rs
static mut COUNTER: u32 = 0; fn add_to_count(inc: u32) { unsafe { COUNTER += inc; } } fn main() { add_to_count(3); unsafe { println!("COUNTER: {COUNTER}"); } }
Listing 19-10: Leer o escribir en una variable static mutable es inseguro
Como con las variables regulares, especificamos la mutabilidad usando la
palabra clave mut
. Cualquier código que lea o escriba desde COUNTER
debe
estar dentro de un bloque unsafe
. Este código se compila e imprime COUNTER: 3
como esperaríamos porque es de un solo hilo. Tener múltiples hilos accediendo
a COUNTER
, probablemente habría condiciones de carrera.
Con datos mutables que son accesibles globalmente, es difícil asegurarse de que no haya carreras de datos, por lo que Rust considera que las variables static mutables son inseguras. Cuando sea posible, es preferible usar las técnicas de concurrencia y los smart pointers seguros para los hilos que discutimos en el Capítulo 16, para que el compilador verifique que los datos a los que se accede desde diferentes hilos se hagan de manera segura.
Implementando un trait inseguro
Podemos usar unsafe
para implementar un trait inseguro. Un trait se considera
inseguro cuando al menos uno de sus métodos tiene algún invariante que el
compilador no puede verificar. Declaramos que un trait es unsafe
agregando la
palabra clave unsafe
antes de trait
y marcando la implementación del trait
como unsafe
también, como se muestra en el Listado 19-11.
unsafe trait Foo { // methods go here } unsafe impl Foo for i32 { // method implementations go here } fn main() {}
Listing 19-11: Definiendo e implementando un trait inseguro
Al utilizar unsafe impl
, estamos prometiendo que mantendremos las invariantes
que el compilador no puede verificar.
Como ejemplo, recordemos los marcadores de traits Sync
y Send
que
discutimos en la sección "Concurrencia extensible con los traits Sync
y
Send
" en el Capítulo
16: el compilador implementa estos traits automáticamente si nuestros tipos se
componen únicamente de tipos Send
y Sync
. Si implementamos un tipo que
contiene un tipo que no es Send
o Sync
, como punteros crudos, y queremos
marcar ese tipo como Send
o Sync
, debemos usar unsafe
. Rust no puede
verificar que nuestro tipo cumpla con las garantías de que se puede enviar
seguramente a través de hilos o acceder desde múltiples hilos; por lo tanto,
debemos hacer esas comprobaciones manualmente e indicarlo con unsafe
.
Acceder a los campos de una union
La última acción que solo se puede realizar con unsafe
es acceder a los
campos de una union. Una union
es similar a una struct
, pero solo un
campo declarado se usa en una instancia particular en un momento dado. Las
unions se usan principalmente para interactuar con unions en código C. Acceder
a los campos de la union es inseguro porque Rust no puede garantizar el tipo de
los datos que se almacenan actualmente en la instancia de la union. Puedes
aprender más sobre las uniones en la Referencia de Rust.
Cuándo usar código inseguro
Utilizar unsafe
para llevar a cabo una de las cinco acciones (superpoderes)
que se acaban de mencionar no está mal ni se desaconseja. Sin embargo, es más
difícil obtener código unsafe
correcto porque el compilador no puede ayudar a
mantener la seguridad de la memoria. Cuando tengas una razón para usar código
unsafe
, puedes hacerlo, y tener la anotación unsafe
explícita hace que sea
más fácil rastrear la fuente de los problemas cuando ocurren.