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 20-1 muestra cómo crear un puntero crudo inmutable y mutable a partir de referencias.
fn main() { let mut num = 5; let r1 = &raw const num; let r2 = &raw mut num; }
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 sin procesar utilizando los operadores de referencia sin
procesar: &raw const num
crea un raw pointer (un puntero sin procesar)
inmutable *const i32
, y &raw mut num
crea un puntero sin procesar mutable
*mut i32
. Debido a que los creamos directamente a partir de una variable
local, sabemos que estos punteros sin procesar en particular son válidos, pero
no podemos hacer esa suposición sobre cualquier puntero sin procesar.
Para demostrar esto, a continuación crearemos un puntero sin procesar cuya
validez no podemos estar tan seguros, utilizando as
para convertir un valor en
lugar de usar los operadores de referencia sin procesar. El Listado 20-2 muestra
cómo crear un puntero sin procesar hacia una ubicación arbitraria en la memoria.
Intentar usar memoria arbitraria resulta en un comportamiento indefinido: podría
haber datos en esa dirección o podría no haberlos, el compilador podría
optimizar el código de manera que no haya acceso a la memoria, o el programa
podría generar un error con una falla de segmentación. Generalmente, no hay una
buena razón para escribir código de este tipo, especialmente en casos donde
puedes usar un operador de referencia sin procesar en su lugar, pero es posible
hacerlo.
fn main() { let address = 0x012345usize; let r = address as *const i32; }
Recuerda que podemos crear punteros crudos en código seguro, pero no podemos
desreferenciar punteros crudos (raw pointers) y leer la memoria a la que
apuntan fuera de un bloque unsafe
.
fn main() { let mut num = 5; let r1 = &raw const num; let r2 = &raw mut num; unsafe { println!("r1 is: {}", *r1); println!("r2 is: {}", *r2); } }
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 20-1 y 20-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 20-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]); }
No podemos implementar esta función utilizando solo Rust seguro. Un intento
podría ser algo como el Listado 20-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);
}
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 20-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`
|
= help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices
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 20-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); }
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 20-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) }; }
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 20-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
usualmente son inseguras de llamar desde el código Rust, por lo que
deben también ser marcadas como unsafe
. 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.
unsafe extern "C" { fn abs(input: i32) -> i32; } fn main() { unsafe { println!("Absolute value of -3 according to C: {}", abs(-3)); } }
Dentro del bloque unsafe extern "C"
, enumeramos los nombres y las firmas de
las funciones externas de otro lenguaje que queremos llamar. La parte "C"
define qué interfaz binaria de aplicación (ABI, por sus siglas en inglés) usa
la función externa: el ABI determina cómo llamar a la función a nivel de
ensamblador. El ABI "C"
es el más común y sigue el ABI del lenguaje de
programación C.
Sin embargo, esta función en particular no tiene consideraciones de seguridad de
memoria. De hecho, sabemos que cualquier llamada a abs
será siempre segura
para cualquier valor de tipo i32
, por lo que podemos usar la palabra clave
safe
para indicar que esta función específica es segura de llamar, incluso si
está en un bloque unsafe extern
. Una vez que hacemos este cambio, llamarla ya
no requiere un bloque unsafe
, como se muestra en el Listado 20-9.
unsafe extern "C" { safe fn abs(input: i32) -> i32; } fn main() { println!("Absolute value of -3 according to C: {}", abs(-3)); }
¡Marcar una función como safe
no la hace intrínsecamente segura! En cambio, es
como una promesa que le haces a Rust de que es segura. Aún así, es tu
responsabilidad asegurarte de que esa promesa se cumpla.
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#[unsafe(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. Esto es inseguro porque podrían ocurrir colisiones de nombres entre bibliotecas al no usar el mangling integrado. Por lo tanto, es nuestra responsabilidad asegurarnos de que el nombre que hemos exportado sea seguro para exportar sin mangling.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() { #[unsafe(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 20-10 muestra un ejemplo de declaración y uso de una variable static con un string slice como valor.
static HELLO_WORLD: &str = "Hello, world!"; fn main() { println!("name is: {HELLO_WORLD}"); }
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 20-11 muestra cómo declarar, acceder y
modificar una variable static mutable llamada COUNTER
.
static mut COUNTER: u32 = 0; /// SAFETY: Calling this from more than a single thread at a time is undefined /// behavior, so you *must* guarantee you only call it from a single thread at /// a time. unsafe fn add_to_count(inc: u32) { COUNTER += inc; } fn main() { unsafe { // SAFETY: This is only called from a single thread in `main`. add_to_count(3); println!("COUNTER: {}", COUNTER); } }
Al igual que con las variables regulares, especificamos la mutabilidad usando la
palabra clave mut
. Cualquier código que lea o escriba en COUNTER
debe estar
dentro de un bloque unsafe
. Este código compila y muestra COUNTER: 3
, como
cabría esperar, porque es de un solo hilo. Sin embargo, si múltiples hilos
accedieran a COUNTER
, probablemente resultaría en data races, lo cual se
considera comportamiento indefinido. Por lo tanto, debemos marcar toda la
función como unsafe
y documentar las limitaciones de seguridad, para que
cualquiera que llame a la función sepa qué puede y qué no puede hacer de forma
segura.
Cada vez que escribimos una función insegura, es idiomático agregar un
comentario que comience con SAFETY
y explique qué debe hacer el llamador para
usar la función de forma segura. De manera similar, siempre que realicemos una
operación insegura, es idiomático escribir un comentario que comience con
SAFETY
para explicar cómo se cumplen las reglas de seguridad.
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 20-12.
unsafe trait Foo { // methods go here } unsafe impl Foo for i32 { // method implementations go here } fn main() {}
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.
Usar Miri para verificar código inseguro
Al escribir código inseguro, es posible que quieras asegurarte de que lo que has escrito sea realmente seguro y correcto. Una de las mejores maneras de hacerlo es usar Miri, una herramienta oficial de Rust para detectar comportamiento indefinido. Mientras que el borrow checker es una herramienta estática que funciona en tiempo de compilación, Miri es una herramienta dinámica que funciona en tiempo de ejecución. Miri verifica tu código ejecutando tu programa o su suite de pruebas, detectando cuándo violas las reglas que comprende sobre cómo debería funcionar Rust.
Usar Miri requiere una versión nightly de Rust (hablamos más sobre esto en el
Apéndice G: Cómo se desarrolla Rust y “Rust Nightly”). Puedes
instalar tanto una versión nightly de Rust como la herramienta Miri escribiendo
rustup +nightly component add miri
. Esto no cambia la versión de Rust que usa
tu proyecto; solo agrega la herramienta a tu sistema para que puedas usarla
cuando lo desees. Puedes ejecutar Miri en un proyecto escribiendo
cargo +nightly miri run
o cargo +nightly miri test
.
Para ver lo útil que puede ser, considera lo que ocurre cuando lo ejecutamos con el Listado 20-11:
$ cargo +nightly miri run
Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
Running `/Users/chris/.rustup/toolchains/nightly-aarch64-apple-darwin/bin/cargo-miri runner target/miri/aarch64-apple-darwin/debug/unsafe-example`
warning: creating a shared reference to mutable static is discouraged
--> src/main.rs:14:33
|
14 | println!("COUNTER: {}", COUNTER);
| ^^^^^^^ shared reference to mutable static
|
= note: for more information, see <https://doc.rust-lang.org/nightly/edition-guide/rust-2024/static-mut-references.html>
= note: shared references to mutable statics are dangerous; it's undefined behavior if the static is mutated or if a mutable reference is created for it while the shared reference lives
= note: `#[warn(static_mut_refs)]` on by default
COUNTER: 3
Miri detecta de manera útil y correcta que tenemos referencias compartidas a datos mutables y nos advierte al respecto. En este caso, no nos dice cómo solucionar el problema, pero nos permite saber que existe un posible inconveniente, lo que nos da la oportunidad de pensar cómo garantizar su seguridad. En otros casos, Miri puede incluso decirnos que cierto código está seguramente mal y hacer recomendaciones sobre cómo corregirlo.
Sin embargo, Miri no detecta todo lo que podrías hacer mal al escribir código inseguro. Por un lado, como es una verificación dinámica, solo detecta problemas en el código que realmente se ejecuta. Esto significa que necesitarás usarlo en conjunto con buenas técnicas de prueba para aumentar tu confianza en el código inseguro que hayas escrito. Por otro lado, no cubre todas las posibles formas en que tu código puede ser inseguro. Si Miri detecta un problema, sabes que hay un error, pero que Miri no detecte un error no significa que no exista. Aun así, Miri puede detectar muchas cosas. ¡Intenta ejecutarlo en los otros ejemplos de código inseguro de este capítulo y observa qué te dice!
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.
Cada vez que escribas código inseguro, puedes usar Miri para ayudarte a tener
más confianza en que el código que has escrito respeta las reglas de Rust.