Documentación con rustdoc

La documentación es una parte importante y muchas veces olvidada de un proyecto de software. Rust cuenta desde el principio con una herramienta destinada a que escribir documentación sea poco doloroso, se trata de rustdoc.

Comentarios en Markdown

Los comentarios de rustdoc empiezan con 3 barras y pueden llevar Markdown. Se escriben encima de la función, estructura, etc que describamos.

/// Perfil almacena los datos del perfil de un usuario en nuestra webapp
struct Perfil{
    username: String,
    password: String,
    url: Option<String>
}

impl Perfil{
    /// Genera un nuevo Perfil
    /// # Ejemplo
    /// ```
    /// let user = Perfil::new("The42","1234");
    /// ```
    pub fn new(u: &str, p: &str) -> Perfil{
        Perfil {username: String::from(u), password: String::from(p), url: None}
    }
}

Mencionar que el código del comentario es sometido a tests también por cargo test. Para generar la documentación basta con escribir cargo doc y Cargo generará la documentación en formato HTML.

Consultando documentación

El mejor sitio para leer la documentación de Rust es Docs.rs. Docs.rs ejecuta cargo doc a todas las crates de Crates.io y las expone al público sin costo.

 

loading...

Tests en Rust

Mientras los programas no puedan verificarse de forma matemática de forma sencilla, la única manera de asegurarse que un programa más o menos funciona es con tests. Rust soporta tests de forma nativa. Gracias a la directiva #[test].

Definiendo una función de test

Una función de test es cualquiera que lleve la directiva #[test]. Normalmente, estos tests se dejan dentro de un módulo con la directiva #[cfg(test)].

fn suma(a: i32,b: i32) -> i32{
    a+b
}

#[cfg(test)]
mod test{

    #[test]
    fn suma(){
        assert_eq!(super::suma(4,5),9);
    }
}

La macro assert_eq! provocará un panic! si sus argumentos no coinciden. También es posible hacer fallar los tests llamando a panic! manualmente. ¿Cómo se ejecutan estos test te preguntarás? Sencillo, con cargo test. Automáticamente Cargo selecciona las funciones de test y las ejecuta todas, generando un informe de tests existosos y tests que han fallado.

Obviamente, existen más opciones dentro de los tests. assert! comprobará que una expresión sea true. #[should_panic] se deberá indicar en aquellas funciones de test en lo que lo correcto sea que ocurra un panic!. Por otro lado, la trait Debug es interesante.

Es posible ejecutar tests de forma manual, con cargo test NOMBRE_TEST.

 

Cargo y módulos en Rust

El software va creciendo poco a poco en un proyecto y es vital que esté bien organizado. Rust incluye un potente sistema de módulos para manejar estas situaciones y Cargo nos ayuda a gestionar las dependencias del proyecto.

Módulos

Los módulos se definen con la palabra reservada mod y por defecto todo es privado. Es necesario indicar pub para hacer esos campos públicos. Con use podemos acortar la manera con la que hacemos referencia a los componentes del módulo. Así pues, use es similar a using namespace de C++.

mod network{
    pub fn version() -> String{
        String::from("Network 1.0.0")
    }
    pub mod server{
        fn private_thing(){

        }
        pub fn public_thing(){

        }
    }
}

fn main() {
    println!("{}",network::version());
    {
        use network::server::public_thing;
        public_thing();
    }
}

Los módulos resultan una opción más interesante si tenemos múltiples archivos. Por norma general, si un módulo se define en el fichero de entrada del compilador, este se busca en dos sitios:

  • Un fichero nombre_del_modulo.rs
  • Un fichero nombre_del_modulo/mod.rs (opción recomendada para módulos de varios archivos)

Por lo que el código anterior puede sustituirse por esto en el fichero principal:

mod network;

fn main() {
    println!("{}",network::version());
    {
        use network::server::public_thing;
        public_thing();
    }
}

Y esto en un fichero network.rs

pub fn version() -> String{
    String::from("Network 1.0.0")
}
pub mod server{
    fn private_thing(){

    }
    pub fn public_thing(){

    }
}

Podemos usar use-as para modificar el nombre con el que se importa una función.

mod network;

fn main() {
    println!("{}",network::version());
    {
        use network::server::public_thing as super_thing;
        super_thing();
    }
}

 

Crates

El código en Rust se organiza en crates, que son colecciones de módulos que se distribuyen. Piensa en ello como si fuesen gemas de Ruby o librerías de C++. Las crates se pueden compartir con la gente. El mayor repositorio de crates de la comunidad Rust es crates.io.

Las crates se pueden generar con rustc o con cargo, aunque es habitual usar la segunda opción. Vamos a ver primero como usaríamos una crate externa. En este caso voy a usar regex. Sé que en esta parte no vas a saber como descargar regex y usarla, pero voy a explicar el código.

extern crate regex;

use regex::Regex;

fn main() {
    let r = Regex::new(r"([A-Za-z0-9])([.]|[_]|[A-Za-z0-9])+@gmail.com").unwrap();
    if r.is_match("pucela_segunda@gmail.com") {
        println!("Correo válido de Gmail");
    }else{
        println!("Correo no válido de Gmail");
    }
}

Básicamente se usa extern crate para importar una crate externa, en este caso regex. Con use lo único que hacemos es acortar la manera de llamar a las funciones. Si no estuviese podríamos poner perfectamente regex::Regex::new en vez de Regex::new y funcionaría.

Cargo

Cargo es una herramienta que cumple dos funciones dentro del ecosistema Rust. Por un lado es un gestor de dependencias y por otro lado gestiona también las compilaciones.

En su parte de gestor de dependencias nos permite especificar que crates necesita el proyecto para compilar, su versión y si la crate lo soporta, con qué características activadas.

En su parte de gestor de compilaciones, realiza la compilación con rustc y añade las flags pertinentes al compilador. También se le pueden especificar cosas más complejas, aunque en Rust no suele ser habitual tener configuraciones de compilación excesivamente complejas.

Todo lo relativo a Cargo se define en el archivo Cargo.toml. Que tiene una estructura similar a esta:

[package]
name = "super_app"
version = "0.1.0"
authors = ["Adrián Arroyo Calle"]

[dependencies]
regex = "0.2.2"

En la sección dependencies puedes añadir línea por línea la crate que te haga falta. Consulta la documentación de cada crate para esto, pues puede haber variaciones.

Comandos básicos de Cargo

Crear un proyecto con Cargo

cargo new –bin mi_proyecto # si queremos que sea ejecutable

cargo new mi_crate # si queremos que sea una crate

Compilar y ejecutar

cargo run

cargo build # solo compilar

cargo build –release # compilar en modo Release

Cargo automáticamente se encarga de obtener las dependencias en la fase de compilación.

Instalar aplicaciones

cargo install APLICACION

Muchas herramientas de pueden distribuir a través de Cargo. Por ejemplo, Racer, un programa que sirve de autocompletado para IDEs como Eclipse o Visual Studio.

Ejecutar tests

cargo test

Generar documentación

cargo doc

Plugins de Cargo

Cargo es extensible gracias a plugins. Algunos interesantes son Clippy, cargo-audit, rustfmt,

Tutorial de Piston, programa juegos en Rust

Ya he hablado de Rust varias veces en este blog. La última vez fue en el tutorial de Iron, que os recomiendo ver si os interesa el tema del desarrollo web backend.

Hoy vamos a hablar de Piston. Piston es una de las librerías más antiguas del ecosistema Rust. Surgida cuando todavía no existía Cargo, esta librería está pensada para el desarrollo de juegos. No es la única que existe en Rust pero sí la más conocida. Piston es una librería que te enseñará Rust de la mejor forma. Y ahora quiero disculparme, porque Piston no es una librería, son un montón, pero eso lo veremos enseguida. En primer lugar creamos un proyecto nuevo con Cargo.

cargo new --bin ejemplo_piston
cd ejemplo_piston

Ahora abrimos el archivo Cargo.toml, vamos a añadir las dependencias necesarias. Las dependencias en Piston son un poco complicadas, veamos:

  • Existen las dependencias core, implementan la API fundamental pero no pueden usarse por separado, son window, input y event_loop. Se usan a través de piston.
  • Los backends de window, existen actualmente 3 backends: glutin, glfw, sdl2. Se importan manualmente.
  • Graphics, una API 2D, no presente en core, pero al igual que las dependencias core necesita un backend.
  • Los backends de graphics son varios: opengl, gfx y glium.
  • Existe una dependencia que nos deja todo montado, piston_window. Esta trae por defecto el core de Piston, glutin, graphics y gfx.
  • Luego existen dependencias extra, como por ejemplo para cargar texturas, estas las podremos ir añadiendo según las necesite el proyecto.

Para simplificar añadimos piston_window únicamente:

 

[package]
name = "piston_example"
version = "0.1.0"
authors = ["Adrián Arroyo Calle"]

[dependencies]
piston_window = "0.59.0"

 

Ahora abrimos el archivo main.rs. Añadimos la crate de piston_window y los módulos que vamos a usar.

extern crate piston_window;

use piston_window::*;
use std::path::Path;

 

Así mismo definimos un par de cosas para el resto del programa, la versión de OpenGL que usará Piston internamente y una estructura para guardar los movimientos de teclado.

const OPENGL: OpenGL = OpenGL::V3_1;

struct Movement{
    up: bool,
    down: bool,
    left: bool,
    right: bool
}

 

En la función main podemos crear la ventana, especificando título y tamaño. Más opciones como V-Sync, pantalla completa y demás también están disponibles.

fn main() {

    let mut window: PistonWindow = WindowSettings::new("Piston - Adrianistan",[640,480])
        .exit_on_esc(true)
        .opengl(OPENGL)
        .build()
        .unwrap();

 

Ahora cargamos la tipografía Sinkin Sans, que vamos a usar para dibujar texto en pantalla. Como hay dos posibles localizaciones comprobamos esos dos lugares antes de salir del programa si no se consigue cargar la fuente.

    let mut glyphs = Glyphs::new(Path::new("SinkinSans.ttf"),window.factory.clone()).unwrap_or_else(|_|{
        let glyphs = Glyphs::new(Path::new("target/debug/SinkinSans.ttf"),window.factory.clone()).unwrap_or_else(|_|{
            panic!("Failed to open the font file. Check that SinkinSans.tff is in the folder");
        });
        glyphs
    });

 

Inicializamos la estructura de movimientos, generamos las dimensiones iniciales del rectángulo (que será un cuadrado en este caso), su color y la posición del ratón.

    let mut mov = Movement{
        up: false,
        down: false,
        left: false,
        right: false
    };

    let mut dims = rectangle::square(50.0,50.0,100.0);
    let mut rect_color = color::BLACK;

    let mut mc: [f64; 2] = [0.0,0.0];

 

Ahora viene la parte importante, el bucle de eventos. El bucle va a funcionar infinitamente generando eventos por el camino (pueden ser eventos de inactividad también). Usamos la función draw_2d para dibujar en 2D. Hay dos maneras de dibujar un rectángulo, en primer lugar tenemos la forma abreviada y en segundo lugar una más completa que permite más opciones. Por último dibujamos el texto usando la fuente y realizando una transformación para que no quede el texto en la posición 0,0.

 while let Some(e) = window.next() {
        window.draw_2d(&e, |c, g| {
            clear([0.5, 0.5, 0.5, 1.0], g);
            rectangle([1.0, 0.0, 0.0, 1.0], // color rojo, rgba
                        [0.0, 0.0, 100.0, 100.0], // dimensiones
                        c.transform, g); // transormacion y donde se va a dibujar

            let rect = Rectangle::new(rect_color);
            rect.draw(dims,&c.draw_state,c.transform,g);
            text(color::BLACK,18,"¡Saludos desde Piston!",&mut glyphs,c.transform.trans(100.0,200.0),g); // aplicamos una transormacion, movemos las X 100 y las Y 200
        });

 

A continuación vamos a tratar cada evento de forma independiente, como todos los métodos devuelven Option, hemos de usar esta sintaxis con Some. En primer lugar tenemos un UpdateEvent, que básicamente nos informa del tiempo delta transcurrido. Recomiendo usar este evento para realizar los cambios en las geometrías, en este caso para mover el rectángulo.

if let Some(upd_args) = e.update_args() {
            let dt = upd_args.dt;
            
                if mov.right {
                    dims[0] += dt*100.0;
                }
                if mov.left {
                    dims[0] -= dt*100.0;
                }
                if mov.up {
                    dims[1] -= dt*100.0;
                }
                if mov.down {
                    dims[1] += dt*100.0;
                }
        }

Los siguientes dos eventos son opuestos, uno se activa cuando pulsamos una tecla y el otro cuando la soltamos. Comprobamos la tecla y modificamos la estructura movement en consecuencia.

if let Some(Button::Keyboard(key)) = e.press_args() {
            if key == Key::W {
                mov.up = true;
            }
            if key == Key::S {
                mov.down = true;
            }
            if key == Key::A {
                mov.left = true;
            }
            if key == Key::D {
                mov.right = true;
            }
        };
        if let Some(Button::Keyboard(key)) = e.release_args() {
            if key == Key::W {
                mov.up = false;
            }
            if key == Key::S {
                mov.down = false;
            }
            if key == Key::A {
                mov.left = false;
            }
            if key == Key::D {
                mov.right = false;
            }
        };

Por último, si queremos comprobar clicks del ratón hacemos algo similar. He añadido código para que cambio el color del rectángulo si pulsamos sobre él.

if let Some(Button::Mouse(mb)) = e.release_args() {
            if mb == MouseButton::Left {
                let x = mc[0];
                let y = mc[1];
                if x > dims[0] && x < dims[0] + dims[2] { if y > dims[1] && y < dims[1] + dims[3] {
                        rect_color = if rect_color == [1.0,0.0,0.0,0.7]{
                            [0.0,1.0,0.0,0.7]
                        } else if rect_color == [0.0,1.0,0.0,0.7] {
                            [0.0,0.0,1.0,0.7]
                        } else{
                            [1.0,0.0,0.0,0.7]
                        }
                    }
                }
                
            }
        }

A continuación un pequeño evento que guarda la última posición del ratón.

        if let Some(mouse_cursor) = e.mouse_cursor_args() {
            mc = mouse_cursor;
        }
    }
}

 

Y con esto ya tenemos hecho un ejemplo en Piston.

Si quieres tener un ejecutable para Windows sin que se muestre primero la consola debes compilar la versión que vas a distribuir con unos parámetros especiales. Si usas Rust con GCC usarás:

cargo rustc --release -- -Clink-args="-Wl,--subsystem,windows"

Si por el contrario usas Visual C++:

cargo rustc --release -- -Clink-args="/SUBSYSTEM:WINDOWS /ENTRY:mainCRTStartup"

 

Piston todavía se encuentra en fuerte desarrollo, en la API estan documentados todos los métodos pero aun así muchas veces no se sabe como hacer ciertas cosas. Piston soporta además 3D, contando con una librería especializada en vóxels. Veremos como evoluciona esta librería.