La gestión de la memoria en Rust

Finalmente ha sido publicada la versión 1.0 de Rust. El lenguaje diseñado por Mozilla basado en 3 principios:

  • Seguridad
  • Concurrencia
  • Rendimiento

Hoy voy a hablar del primer principio, la razón principal para querer ser un sustituto de C++. Porque C++ está bien, pero puedes liarla mucho si no sabes lo que haces.

Rust

El concepto de dueño

En Rust todo tiene un dueño. No puede haber más de uno ni ninguno, debe ser uno exactamente.

Hasta aquí todo es sencillo. Ahora pasaremos la variable A a otra función.

El programa compila y nos da el resultado, que es 9. En los lenguajes de bajo nivel las variables pueden usar memoria del stack o del heap. Un pequeño repaso sobre sus diferencias.

Stack
  • Se reserva su espacio en RAM cuando el programa arranca
  • Son más rápidas de acceder
  • No se les puede cambiar el tamaño
  • Son más seguras
Heap
  • Se debe reservar manualmente la RAM cuando queramos
  • Son más lentas de acceder
  • Pueden cambiar su tamaño en tiempo real
  • Son menos seguras. Pueden dar lugar a fugas de memoria.

En este último caso, la variable A cuyo dueño es main() le pasa la propiedad temporalmente a sumar(). La propiedad se devuelve a main() rápidamente y esta garantizado que así suceda. El compilador lo permite. Veamos ahora un ejemplo más complejo.

Veamos ahora un código más complejo

Por supuesto el código compila pero este de aquí abajo no y solo he cambiado una línea.

La razón es que cuando creamos la estructura de App por primera vez le prestamos config a la estructura app. Así la función main no le puede pasar la propiedad a backup porque ya se la prestó a app.

Referencias

Para solucionar este problema Rust usa las referencias. Así la propiedad de config seguirá siendo main() pero lo podrán usar las estructuras app y backup. Para usar referencias usamos el símbolo &.

La estrucura ahora acepta &Config en vez de Config. Es de decir usa una referencia en vez de un valor. Sin embargo esto no compilará. El compilador normalmente deduce si es posible hacer una referencia a algo no existente, un fallo común en C++. En caso de tener dudas no compilará. Rust es bastante inteligente pero no es mágico. En el caso de la estructura App, es necesario indicar que la propiedad config vivirá el mismo tiempo que la estructura.

He usado la anotación de tiempo llamada a. Puedes poner cualquier nombre pero a es muy corto.

Implementaciones y referencias

Voy a introducir un concepto de Rust que son las implementaciones. Para haceros una idea rápida, serían como clases de C++, pero solo alojan funciones.

He creado dos funciones para implementar App. Son idénticas salvo por un pequeño detalle, una toma el valor self (como this en C++) por referencia y la otra toma el valor directamente.

Compila y funciona. Cambiemos el orden.

Ya no compila. La razón es que cuando llamamos a delete() estamos prestando app entera. Ahora delete() es la dueña de app y cuando salimos de la función eliminamos app porque si su dueña ha muerto, app también debe morir (no es tan sangriento como pensais). Rust lo detecta y delete() será la última función que podemos llamar de app. Por cierto si os preguntais como funcionan las implementaciones en Rust (que no son clases), este código haría lo mismo llamando a funciones estáticas. Quizá así veais mejor como se pasa el concepto de dueños y préstamos.

Diversión con punteros en el heap

Todo estas variables eran del stack que siempre es la manera más sencilla de operar. Vamos ahora a ver como funcionaría esto con punteros. Los punteros operan como variables en el stack que hacen referencia a partes de la memoria que están en el heap. En Rust podemos operar con punteros con máxima seguridad pues todo lo aplicable a variables en el stack sigue siendo válido. Solo hay un dueño y podemos hacer referencias, aunque quizá necesitemos marcar el tiempo de vida manualmente.

Ahora el valor 42 estará en el heap y con puntero podremos acceder a él. Sin embargo como es lógico, no podemos operar directamente con él.

Para operar el valor directamente tenemos que derreferenciarlo. Se usa *

Así que esta operación sería correcto. Nótese el uso de mut para permitir editar el valor. En Rust por defecto las variables no son mutables. Ese privilegio tiene que ser declarado por adelantado.

Como curiosidad mencionar que la macro println! (en Rust si algo termina con ! es una macro) acepta puntero o *puntero indistintamente ya que se da cuenta si es necesario derreferenciar o no.

El problema final

¿Qué pasaría si copiamos un puntero en otro? Pues como un valor en el heap solo puede tener un dueño, la propiedad será del último puntero.

Como curiosidad, este es un curioso método para bloquear en un determinado momento el acceso de escritura a nuestro puntero aunque es fácil volver a obtener el acceso a escritura con un nuevo cambio de dueño.

Conclusiones

Podemos ver que es un lenguaje que presta mucha atención a la seguridad. C++ es mucho más liberal en ese sentido y Mozilla cree que es un problema a la hora de desarrollar grandes aplicaciones. ¿Qué te ha parecido? Si tienes alguna duda no titubees y pregunta.

Crea tu propio lenguaje de programación

Esta entrada la escribí en DesdeLinux hace ya un tiempo y la quiero conservar aquí. La entrada no es la original sino que la he modificado para que siga vigente – MAYO DE 2015

LenguajesProgramacion

Después de escribir el primer artículo sobre cómo crear tu propio sistema operativo, alguien me dijo que si podía hacer un artículo sobre cómo crear un lenguaje de programación. Al principio no hice mucho caso, pero ahora y por otros caminos he aprendido bastante más sobre la creación de los lenguajes de programación. Así pues, vamos a hacer un lenguaje de programación básico, fácilmente empotrable en otros programas y que funcione con una máquina virtual que también diseñaremos. Hoy nos toca hacer la máquina virtual más básica.

Probablemente te preguntes: “¿una máquina virtual? ¿Pero eso no es muy difícil y además ralentiza los programas?” Al contrario, una máquina virtual simple es muy sencilla y relativamente rápida. He elegido Rust como lenguaje para la máquina virtual. Pero, ¿qué es Rust?

Rust es un lenguaje de programación que está enfocado en la seguridad en las ejecuciones, así que utilizándolo será prácticamente imposible que alguien consiga cerrar la máquina virtual. Es un lenguaje compilado en desarrollo creado por Mozilla. Servo, el sustituto de Gecko, se está desarrollando en él. Todavía puede cambiar su sintaxis pero el código que voy a usar va a mantenerse hasta la primera versión estable.

Rust se instala en Linux de manera sencilla. Antes se podía usar un PPA pero ahora el script de RustUp es muy bueno y se encarga de todo.

¿Cómo funciona una máquina virtual?

Si sabes como funciona el mundo en ensamblador es exactamente igual, con el stack o la pila. Si no, te lo explico. Imaginémonos el siguiente código:

El ordenador no entiende lo que significa 2+3, ni tampoco sabe qué orden hay que seguir. Los ordenadores funcionan con pilas o stacks en los que se van acumulando datos y se van sacando continuamente. Ese código en nuestra máquina virtual debería ser algo parecido a esto:

Básicamente, pondríamos el 2 en la pila en lo alto, el 3 también. ADD sacaría (es decir, lo elimina de la pila y obtiene su valor) los 2 últimos elementos de la pila y añadiría el resultado en lo alto de la pila. PRINT cogería el último elemento de la pila y lo usaría para mostrárnoslo. Ahora hagamos eso en Rust.

Ahora es cuando deberías descargarte el código fuente que está en GitHub. Voy a empezar por el archivo vm.rs

Primeramente deberemos definir un lenguaje para el Bytecode, podríamos usar uno ya existente como el de Java o el CLR de .NET/Mono, pero vamos a crear nosotros uno más básico.

Usamos notación hexadecimal para cada instrucción. Para hacer la traducción entre el código hexadecimal y la instrucción voy a usar la librería (crate en el argot de Rust) de enum_primitive. Antes se podía usar #[derive(FromPrimitive)] pero en Rust 1.0 no está disponible.

Ahora debemos hacer una función que ejecute cada una de esas instrucciones. Para ello debemos leer un byte y compararlo con las instrucciones que tenemos en la enumeración. Si se encuentra alguna que exista se debe ejecutar su acción.

Eso hacemos para leer cada byte individualmente y para ejecutarlas:

Como ven, diferenciamos si antes se nos dio la orden de PUSH (nuestra orden INTEGER), el siguiente byte será llevado completamente a la pila. Ahí estamos usando dos funciones que no les he enseñado, self.pop() y self.push(), que se encargan obviamente de manejar el stack.

No son muy complejas, pero la función de pop tiene mecanismos de detección de errores. De hecho, en Rust, si quitásemos esos mecanismos nos daría un error de compilación. Ahora simplemente debemos llamar en un programa a Perin (nuestra máquina virtual) y que ejecute un bytecode.

Ese bytecode puede ser leído de un fichero, pero aquí para simplificar lo he almacenado en una variable. Si lo ejecutamos nos dará el resultado esperado:

Todo el código está disponible en GitHub bajo la Apache License 2.0: https://github.com/AdrianArroyoCalle/perin. Para compilar deben tener Cargo instalado y poner: