Adrianistán

El blog de Adrián Arroyo


Diversión con punteros en Rust: bloques unsafe

- Adrián Arroyo Calle

Hola, soy Adrián Arroyo y bienvenidos a un nuevo episodio de Diversión con Punteros.

Hoy vamos a hablar de un tema apasionante. Los bloques unsafe de Rust así como de los raw pointers. ¿Has programado en C? Si es así, los raw pointers de Rust son exactamente iguales a los punteros de C. Si no sabes lo que es un puntero, te lo explico.

¿Qué es un puntero?


Un puntero es un tipo de variable que en vez de almacenar el dato, almacena la posición en memoria donde se encuentra el dato.

En lenguajes en lo que todo es un objeto (como Python), nunca trabajamos con los datos reales, sino siempre con punteros, pero el lenguaje lo gestiona de forma automática. En lenguajes más cercanos al metal por contra sí que suele dejarse esta opción.

Nuestro puntero es la variable que contiene 0x00ffbea0 y que apunta a la dirección de memoria donde se encuentra el dato

Rust tiene distintos tipos de punteros: Box, Rc, Arc, Vec, ... Estos punteros son transparentes al usuario y muchas veces no tenemos que preocuparnos de su funcionamiento. Sin embargo, muchas veces queremos tener un control más fino del ordenador. Esto lo lograremos con los raw pointers. Se trata de punteros con los que podemos operar y desreferenciar.

Crear raw pointers no supone ningún problema, pero acceder al valor al que apuntan en memoria sí. Podría darse el caso de que no existiera valor alguno o hubiese sido modificado. En los punteros normales, el compilador de Rust se encarga de que no ocurra, pero en los raw pointers el compilador no lo puede saber. Es por ello, que para acceder al valor de un raw_pointer necesitas usar bloques de código unsafe, código inseguro en Rust.

Creando un raw pointer


Lo primero que hay que saber es que existen dos tipos de raw pointers en Rust, los mutables y los inmutables.

Los punteros inmutables tienen el tipo *const T y los mutables el tipo *mut T.
fn main(){ 
let numero = 5;
let puntero = &numero as *const i32;
println!("Address: 0x{:x}", puntero as usize);
println!("Value: {}",numero);
}

 

En este ejemplo, creamos una variable con valor 5 y le creamos un puntero, que contiene la dirección de memoria donde está el dato. Para representar la dirección de memoria se suele usar la notación hexadecimal. Antes debemos hacer un cast a usize. usize es un tipo en Rust cuyo tamaño depende de la máquina en cuestión (32 bits en máquinas de 32 bits, 64 bits en máquinas de 64 bits), siendo usado para representar direcciones de memoria, puesto que tiene el tamaño exacto para almacenarlas.

Hasta ahora no hemos usado unsafe. Esto es porque no hemos probado a acceder al valor. Para acceder a un valor, o deferrenciar, usamos el operador *.


fn main(){
let numero = 5;
let puntero = &numero as *const i32;
println!("Address: 0x{:x}", puntero as usize);
println!("Value: {}",numero);
unsafe{
println!("Value: {}",*puntero);
}
}


Ambos prints imprimen 5. Hasta aquí no hemos hecho nada interesante con punteros. Todo esto era más fácil hacerlo sin punteros. Veamos alguna aplicación práctica de los punteros.

Modificar datos sin control


Si te pongo este código, ¿me puedes decir que salida dará?


fn main(){
let mut numero = 5;
let puntero = &mut numero as *mut i32;
println!("Address: 0x{:x}", puntero as usize);
unsafe{
scary_things(puntero);
}
println!("Value: {}",numero);
}


Uno podría pensar que como en ningún sitio reasignamos numero, y numero es una variable de tipo i32, que implementa Copy, es imposible modificarle el valor. Y eso es correcto en las reglas de Rust normales, pero en unsafe, podemos pasar el puntero hacia otras funciones (los punteros también son Copy, ocupan el tamaño de un usize). Y esas funciones pueden modificar los datos en memoria a su antojo. Así, pues, la respuesta correcta es indeterminado. Hacer esto es una mala práctica, pero en ocasiones se puede ganar rendimiento o interactuar con una librería de C usando estos métodos.


unsafe fn scary_things(p: *mut i32) {
*p = 12;
}

fn main(){
let mut numero = 5;
let puntero = &mut numero as *mut i32;
println!("Address: 0x{:x}", puntero as usize);
unsafe{
scary_things(puntero);
}
println!("Value: {}",numero);
}


Esta sería la versión completa del programa.

Aritmética de punteros


Una vez tenemos acceso a memoria podemos acceder a cualquier parte de memoria (en sistemas operativos modernos, memoria que esté asignada a nuestro programa). En C simplemente podíamos operar con el puntero como si fuese un número, con sumas, restas, multiplicaciones y divisiones. Estas operaciones eran un poco traicioneras porque eran relativas a la máquina. Sumar 1 a un puntero de int equivalía en realidad a sumar 4 al puntero en una máquina de 32 bits. En Rust esto no se permite, pero a cambio tenemos métodos que nos permiten hacer lo mismo. El más importante es offset. El offset nos permite desplazarnos por la memoria hacia delante y hacia atrás.


fn main(){
let mut numero = 5;
let b = 35;
let c = 42;
let puntero = &mut numero as *mut i32;
println!("Address: 0x{:x}", puntero as usize);
unsafe{
*puntero.offset(1) = 120;
}
println!("Value: {}",numero);
println!("Value: {}",b);
println!("Value: {}",c);
}


Este programa parte de una suposición para funcionar. Y es que numero, b y c están contiguos en memoria y en el mismo orden que como los que he declarado. En el puntero tenemos la dirección a numero, es decir, a 5. Sin embargo, si avanzamos en la memoria una posición llegaremos a al 35, y si avanzamos dos, llegamos a 42. Entonces podemos editar el contenido de esa memoria. Al acabar el programa b vale 120. Hemos modificado el valor y ni siquiera b se había declarado como mut. Esto os recuerdo, usadlo solo en casos excepcionales.

Reservar memoria al estilo C


Estas cosas empiezan a tener utilidad en cuanto podemos usar memoria dinámica al estilo C, es decir, con malloc, free, calloc y compañía. El equivalente a malloc en Rust suele ser Box o Vec y es lo que debemos usar. Box sabe que espacio en memoria tiene que reservar de antemano y Vec ya está preparado para ir creciendo de forma segura.


extern crate libc;

use libc::{malloc,free};
use std::mem::size_of;

fn main(){
unsafe {
let puntero = malloc(10*size_of::<i32>()) as *mut i32;
for i in 0..10 {
*puntero.offset(i) = 42;
}
for i in 0..10{
println!("{}",*puntero.offset(i));
}
free(puntero as *mut libc::c_void);
}
}


En este caso usamos malloc como en C para generar un array de forma dinámica con espacio suficiente para almacenar 10 elementos de tamaño i32.

Con esto ya hemos visto el lado oscuro de Rust, la parte unsafe. No hemos visto como llamar a funciones de C directamente, algo que también require usar bloques unsafe.

Como vemos, Rust no nos limita a la hora de hacer cualquier cosa que queramos, solo que nos reduce a los bloques unsafe, para que nosotros mismos tengamos mejor control de lo que hagamos.

Comentarios

[&#8230;] de hacen desde un bloque unsafe ya que se van a usar punteros al estilo C, de forma insegura. Hace tiempo escribí sobre ello así que voy a saltarme esa [&#8230;]

Añadir comentario

Todos los comentarios están sujetos a moderación