Tutorial de Rocket, echa a volar tus webapps con Rust

Previamente ya hemos hablado de Iron como un web framework para Rust. Sin embargo desde que escribí ese post ha surgido otra librería que ha ganado mucha popularidad en poco tiempo. Se trata de Rocket. Un web framework que propone usar el rendimiento que ofrece Rust sin sacrificar la facilidad de uso de otros lenguajes.

Rocket lleva las pilas cargadas

A diferencia de Iron, Rocket incluye bastantes prestaciones por defecto con soporte para:

  • Plantillas
  • Cookies
  • Formularios
  • JSON
  • Soporte para rutas dinámicas

Un “Hola Mundo”

Rocket necesita una versión nightly del compilador de Rust. Una vez lo tengas creamos una aplicación con Cargo.

cargo new --bin rocket_app

Ahora modificamos el fichero Cargo.toml generado en la carpeta rocket_app para añadir las siguientes dependencias:

rocket = "0.2.4"
rocket_codegen = "0.2.4"
rocket_contrib = "*"

Editamos el archivo src/main.rs para que se parezca algo a esto:

#![feature(plugin)]
#![plugin(rocket_codegen)]

extern crate rocket;

#[get("/")]
fn index() -> &'static str {
    "El cohete ha despegado"
}

fn main() {
    rocket::ignite().mount("/",routes![index]).launch();
}

Con esto iniciamos Rocket y dejamos a la función index que gestione las peticiones GET encaminadas a /. El servidor devolverá El cohete ha despegado.

Ahora si ejecutamos cargo run veremos algo similar a esto:

Vemos que el servidor ya está escuchando en el puerto 8000 y está usando todos los cores (en mi caso 4) del ordenador.

Configurar Rocket

Rocket dispone de varias configuraciones predeterminadas que afectan a su funcionamiento. Para alternar entre las configuraciones debemos usar variables de entorno y para modificar las configuraciones en sí debemos usar un fichero llamado Rocket.toml.

Las configuraciones por defecto son: dev (development), stage (staging) y prod (production). Si no indicamos nada, Rocket se inicia con la configuración dev. Para arrancar con la configuración de producción modificamos el valor de ROCKET_ENV.

ROCKET_ENV=prod cargo run --release

Sería el comando para arrancar Rocket en modo producción. En el archivo Rocket.toml se puede modificar cada configuración, estableciendo el puerto, el número de workers y parámetros extra pero no vamos a entrar en ello.

Rutas dinámicas

Rocket soporta rutas dinámicas. Por ejemplo, si hacemos GET  /pelicula/Intocable podemos definir que la parte del nombre de la película sea un parámetro. Esto hará que la función encargada de /pelicula/Intocable y de /pelicula/Ratatouille sea la misma.

#![feature(plugin)]
#![plugin(rocket_codegen)]

extern crate rocket;

#[get("/pelicula/<pelicula>")]
fn pelicula(pelicula: &str) -> String {
    format!("Veo que te gusta {}, a mi también!",pelicula)
}

#[get("/")]
fn index() -> &'static str {
    "El cohete ha despegado"
}

fn main() {
    rocket::ignite().mount("/",routes![index,pelicula]).launch();
}

Los argumentos de la función son los parámetros de la petición GET. ¿Qué pasa si no concuerda el tipo de la función con lo que se pasa por HTTP? Nada. Sencillamente Rocket ignora esa petición, busca otra ruta (puede haber sobrecarga de rutas) y si encuentra otra que si satisfaga los parámetros será esa la escogida. Para especificar el orden en el que se hace la sobrecarga de rutas puede usarse rank. En caso de no encontrarse nada, se devuelve un error 404.

POST, subir JSON y formularios

Rocket se integra con Serde para lograr una serialización/deserialización con JSON inocua. Si añadimos las dependencias serde, serde_json y serde_derive al fichero Cargo.toml podemos tener un método que acepete una petición POST solo para mensajes del tipo application/json con deserialización incorporada.

#![feature(plugin)]
#![plugin(rocket_codegen)]

#[macro_use] extern crate rocket_contrib;
#[macro_use] extern crate serde_derive;
extern crate serde_json;
extern crate rocket;

use rocket_contrib::{JSON, Value};

#[derive(Serialize,Deserialize)]
struct User{
    name: String,
    email: String
}

#[post("/upload", format="application/json", data="<user>")]
fn upload_user(user: JSON<User>) -> JSON<Value> {
    JSON(json!({
        "status" : 200,
        "message" : format!("Usuario {} registrado con éxito",user.email)
    }))
}

fn main() {
    rocket::ignite().mount("/",routes![upload_user]).launch();
}

Si el JSON no se ajusta a la estructura User simplemente se descarta devolviendo un error 400.

Lo mismo que es posible hacer con JSON puede hacerse con formularios usando el trait FromForm.


#![feature(plugin,custom_derive)]
#![plugin(rocket_codegen)]

#[macro_use] extern crate rocket_contrib;
#[macro_use] extern crate serde_derive;
extern crate serde_json;
extern crate rocket;

use rocket_contrib::{JSON, Value};
use rocket::request::{FromForm, Form};

#[derive(FromForm)]
struct User{
    name: String,
    email: String
}

#[post("/upload", data="<user>")]
fn upload_user(user: Form<User>) -> String {
    format!("Hola {}",user.get().name)
}

fn main() {
    rocket::ignite().mount("/",routes![upload_user]).launch();
}

Errores

En Rocket, como es lógico, es posible crear páginas personalizadas para cada error.

#![feature(plugin,custom_derive)]
#![plugin(rocket_codegen)]

#[get("/")]
fn index() -> &'static str {
    "El cohete ha despegado"
}

#[error(404)]
fn not_found() -> &'static str {
    "La página no ha podido ser encontrada"
}

fn main() {
    rocket::ignite().mount("/",routes![index]).catch(errors![not_found]).launch();
}

La lista de métodos que manejan errores hay que pasarla en el método catch de rocket::ignite

Respuestas

Rocket nos permite devolver cualquier cosa que implemente el trait Responder. Algunos tipos ya lo llevan como String, File, JSON, Option y Result. Pero nada nos impide que nuestros propios tipos implementen Responder. Con Responder tenemos el contenido y el código de error (que en la mayoría de casos será 200). En el caso de Result es muy interesante, pues si Err contiene algo que implementa Responder, se devolverá la salida que implemente también, pudiendo así hacer mejores respuestas de error, mientras que si no lo hacen se llamará al método que implemente el error 500 de forma genérica. Con Option, si el valor es Some se devolverá el contenido, si es None se generará un error 404.

#![feature(plugin,custom_derive)]
#![plugin(rocket_codegen)]

#[macro_use] extern crate rocket_contrib;
extern crate rocket;

use rocket::response::{self, Responder, Response};
use std::io::Cursor;
use rocket::http::ContentType;

struct Pelicula{
    nombre: &'static str,
    pais: &'static str
}

impl<'r> Responder<'r> for Pelicula{
    fn respond(self) -> response::Result<'r> {
        Response::build()
        .sized_body(Cursor::new(format!("La película {} se hizo en {}",self.nombre,self.pais)))
        .header(ContentType::new("text","plain"))
        .ok()
    }
}

#[get("/pelicula/<pelicula>")]
fn pelicula(pelicula: &str) -> Result<Pelicula,String> {
    let intocable = Pelicula{
        nombre: "Intocable",
        pais: "Francia"
    };
    let madMax = Pelicula{
        nombre: "Mad Max",
        pais: "Estados Unidos"
    };
    match pelicula {
        "Intocable" => Ok(intocable),
        "Mad Max" => Ok(madMax),
        _ => Err(format!("No existe esa película en nuestra base de datos"))
    }
}

#[get("/")]
fn index() -> Result<String,String> {
    Err(format!("No implementado"))
}

#[error(404)]
fn not_found() -> &'static str {
    "La página no ha podido ser encontrada"
}

fn main() {
    rocket::ignite().mount("/",routes![index,pelicula]).catch(errors![not_found]).launch();
}

Este ejemplo para /pelicula/Intocable devolverá: La película Intocable se hizo en Francia mientras que para /pelicula/Ratatouille dirá No existe esa película en nuestra base de datos.

También es posible devolver plantillas. Rocket se integra por defecto con Handlebars y Tera, aunque no es muy costoso añadir cualquier otra como Maud.

Conclusión

Rocket es un prometedor web framework para Rust, bastante idiomático, que se integra muy bien con el lenguaje. Espero con ansia las nuevas veriones. Es posible que la API cambie bastante hasta que salga la versión 1.0, no obstante así es como ahora mismo funciona.

loading...

Rust en 5 minutos – #PicnicCode2017

El pasado 17 de marzo fue el Picnic Code en la Universidad de Valladolid. En el evento, organizado por el GUI y Cylicon Valley, tuve el honor de dar una Lightning Talk. Se trata de una charla de 5 minutos muy rápidos para exponer una idea. Mi Lightning Talk titulada Rust en 5 minutos iba dirigida a enseñar, sin entrar en muchos detalles, aquellas características que hacen de Rust un lenguaje seguro. No estaba nervioso hasta que subí al escenario… ¡y entonces ya empecé a estarlo! Hay algunos fallos frutos de los nervios y las diapositivas… bueno, podían haber funcionado mejor.

En cualquier caso, estáis invitados a ver Rust en 5 minutos.

Tutorial de Maud, motor de plantillas HTML para Rust

Seguimos aprendiendo en el blog sobre interesantes proyectos hechos para Rust. Ya hemos visto Iron, Piston y Neon. Hoy veremos Maud, un potente motor de plantillas que se centra en la eficiencia. Maud se compara a otras soluciones como Razor, ERB, Liquid,  Handlebars o Jade pero esta vez escribiremos nuestro HTML en Rust. ¿Locura? No, y de hecho funciona de forma bastante transparente. Vamos a verlo en acción

Comparativa de velocidad de diferentes motores. Maud es el más rápido (menos es mejor)

Instalando Maud

Maud usa plugins del compilador, una característica que a día de hoy no está activado ni en el canal estable ni el canal beta, solamente en el canal nightly. Para obtener una copia de Rust nightly lo ideal es usar Rustup.

Una vez hecho eso, creamos un nuevo proyecto y añadimos las dependencias de Maud al fichero Cargo.toml.

maud = "*"
maud_macros = "*"

Una simple plantilla

Ahora abrimos el archivo src/main.rs y vamos a empezar a usar Maud.

#![feature(plugin)]
#![plugin(maud_macros)]

extern crate maud;

fn main(){
        let name = "Adrián";
        let markup = html!{
            p { "Hola, soy " (name) " y estoy usando Maud"}
        };
        println!("{}", markup.into_string());
}

La potencia de Maud se ve en la mega-macro html!. En esta macro escribiremos la plantilla que será compilada de forma nativa a Rust, lo que nos asegura una velocidad de ejecución excepcional. En este caso la salida será una etiqueta P de párrafo con la variable interpolada.

Simple, ¿no?

PreEscaped y otros elementos básicos

Por defecto en Maud todos el texto se convierte a HTML seguro. Es decir, no se pueden introducir etiquetas nuevas en el texto. Si por alguna razón necesitásemos añadir etiquetas nuevas podemos usar PreEscaped, que no realiza esta transformación de seguridad. Veamos el siguiente código:

#![feature(plugin)]
#![plugin(maud_macros)]

extern crate maud;

use maud::PreEscaped;

fn main(){
        let name = "Adrián";
        let markup = html!{
                p { "Hola, soy " (name) " y estoy usando Maud" }
                p { "<h5>Esto no funcionará</h5>" }
                p { (PreEscaped("<h5>Esto sí funcionará</h5>")) }
        };
        println!("{}", markup.into_string());
}

El primer H5 se convertirá a código HTML seguro, es decir, no añadirá la etiqueta, en cambio se verá h5 en la web. Por contra con PreEscaped se añadirá la etiqueta h5 tal cual.

Los elementos son muy fáciles de añadir en Maud y por lo general no deberías usar PreEscaped salvo contadas ocasiones. Veamos como añadir más etiquetas.

#![feature(plugin)]
#![plugin(maud_macros)]

extern crate maud;

fn main(){
        let name = "Adrián";
        let markup = html!{
                p { "Hola, soy " (name) " y estoy usando Maud" }
                p {
                        "Si la montaña no viene a Mahoma"
                        br /
                        "Mahoma va la montaña"
                        small em "Atribuido a Francis Bacon"
                }
        };
        println!("{}", markup.into_string());
}

En este ejemplo vemos como las etiquetas que no llevan texto como BR o INPUT debemos cerrarlas con una barra. Por otro lado es posible aglutinar varios niveles de etiquetas en una sola línea ( SMALL->EM->Texto).

Atributos, clases e IDs

En Maud es posible asignar atributos también, usando literales o variables. Para los atributos de texto la sintaxis es muy parecida a HTML.

#![feature(plugin)]
#![plugin(maud_macros)]

extern crate maud;

fn main(){
        let name = "Adrián";
        let amazon = "http://www.amazon.com";
        let markup = html!{
                p { "Hola, soy " (name) " y estoy usando Maud" }
                p {
                        "Este texto contiene enlaces a "
                        a href="http://www.google.com" "Google"
                        " y a "
                        a href=(amazon) "Amazon"
                }
        };
        println!("{}", markup.into_string());
}

Además existen en HTML atributos vacíos. Es decir, atributos que con su sola presencia basta y normalmente no llevan valor asignado.

#![feature(plugin)]
#![plugin(maud_macros)]

extern crate maud;

fn main(){
        let name = "Adrián";
        let allow_editing = true;
        let markup = html!{
                p { "Hola, soy " (name) " y estoy usando Maud" }
                p contenteditable?[allow_editing] {
                }
        };
        println!("{}", markup.into_string());
}

En este caso el atributo contenteditable se añade si la variable allow_editing es true. Si queremos añadir atributos vacíos en cualquier circunstancia podemos simplemente quitar [allow_editing] y dejar p contenteditable? {}.

Los IDs y las clases se añaden usando la sintaxis esperable, puntos y almohadillas.

#![feature(plugin)]
#![plugin(maud_macros)]

extern crate maud;

fn main(){
        let name = "Adrián";
        let markup = html!{
                p { "Hola, soy " (name) " y estoy usando Maud" }
                p.red.font-big#editor contenteditable? {
                }
        };
        println!("{}", markup.into_string());
}

Estructuras de control

Maud soporta las estructuras de control básicas de Rust, if/else, if/let, for in y match.

#![feature(plugin)]
#![plugin(maud_macros)]

extern crate maud;

fn main(){
        let loggedIn = false;
        let email = Some("mariscal@example.com");
        let fonts = ["Arial","Times New Roman","Verdana"];
        let markup = html!{
                @if loggedIn {
                        h1 { "Has iniciado sesión" }
                } @else {
                        h1 { "Por favor, inicia sesión primero" }
                }

                @if let Some(mail) = email {
                        p { "Su email es " (mail) }
                }
                ol {
                        @for font in &fonts {
                                li (font)
                        }
                }

        };
        println!("{}", markup.into_string());
}

Como vemos, Maud posee suficientes características para ser interesante. Maud además permite extender el motor para representar cualquier tipo de dato. Por defecto Maud mirá si está implementado std::fmt::Display pero si queremos añadir etiquetas extra este método no funcionará. En cambio si se implementa la trait maud::Render tendrás control total sobre como va a mostrar Maud las variables de ese tipo. En la crate maud_extras se encuentran implementaciones por ejemplo de Markdown para Maud.

Maud se integra además con web frameworks de Rust, en especial con Iron y con Rocket. Sin duda, una de mis crates favoritas.

Tutorial de Neon – Combina Node.js con Rust

Hoy en día muchas webs se diseñan con Node.js. Es una solución fantástica para respuestas rápidas pero numerosos benchmarks han demostrado que su rendimiento empeora en respuestas complejas. Estos mismos benchmarks recomiendan usar Java o .NET si preveemos que nuestra aplicación web va a generar respuestas complejas. Sin embargo renunciar a las ventajas de Node.js no es del agrado de muchos. Afortunadamente hay otra solución, usar Rust. Todo ello gracias a Neon.

Con Neon podemos generar módulos para Node.js que son escritos y compilados en Rust con las ventajas que supone desde un punto de vista de rendimiento y con la certeza de que en Rust la seguridad está garantizada.

Usando Neon puedes desarrollar tu aplicación en Node.js y si alguna parte tiene problemas de rendimiento sustituirla por su equivalente en Rust. Para el ejemplo voy a hacer un módulo de Markdown.

Instalando Neon

En primer lugar instalamos la herramienta de Neon desde npm.

npm install -g neon-cli

Una vez esté instalado podemos usar la herramienta de Neon para construir un esqueleto de módulo. Este esqueleto tendrá dos partes, un punto de entrada para Node.js y otro para Rust.

neon new PROYECTO

Hacemos un npm install como nos indica. Esto no solo obtendrá dependencias de Node.js sino que también se encargará de compilar el código nativo en Rust.

El código Node.js

Nuestro módulo contiene un archivo de Node.js que sirve de punto de entrada. Allí lo único que se debe hacer es cargar el módulo en Rust y hacer de pegamento. Puede ser algo tan simple como esto:

var addon = require("../native");

module.exports = addon; // se exportan todos los métodos del módulo nativo

Aunque si queremos añadir un tratamiento específico también es posible.

var addon = require("../native");

module.exports = {
    render: function(str){
        return addon.render(str);
    }
}

El código en Rust

Nos dirigimos ahora al archivo native/src/lib.rs. Ahí definimos los métodos nativos que va a tener el módulo. Lo hacemos a través de la macro register_module!.

register_module!(m,{
    m.export("render",render)
});

Ahora vamos a implementar la función render, que toma el texto en Markdown y lo devuelve en HTML.

fn render(call: Call) -> JsResult<JsString> {
    let scope = call.scope; // obtener el contexto
    let md: Handle<JsString> = try!(try!(call.arguments.require(scope,0)).check::<JsString>()); // obtener primer argumento como JsString. aquí puede hacerse tratamiento de fallos
    let string = md.value(); // Pasar JsString a String
    let html: String = markdown::to_html(&string); // usamos la crate markdown para renderizar a html
    Ok(JsString::new(scope, &html).unwrap()) // devolvemos un JsString con el contenido del HTML
}

Las funciones que interactuan con Node deben devolver un JsResult de un tipo JsXXX, por ejemplo, JsString, JsUndefined o JsInteger. Siempre aceptan un argumento llamado de tipo Call que nos da toda la información necesaria y que podemos usar para sacar argumentos. El scope o contexto es muy importante y lo deberemos usar en las funciones que interactúen con Node.

Código completo del fichero Rust

#[macro_use]
extern crate neon;
extern crate markdown;

use neon::vm::{Call, JsResult};
use neon::js::JsString;
use neon::mem::Handle;

fn render(call: Call) -> JsResult<JsString> {
    let scope = call.scope;
    let md: Handle<JsString> = try!(try!(call.arguments.require(scope,0)).check::<JsString>());
    let string = md.value();
    let html: String = markdown::to_html(&string);
    Ok(JsString::new(scope, &html).unwrap())
}

register_module!(m, {
    m.export("render", render)
});

Y no te olvides de añadir la dependencia markdown al fichero Cargo.toml.

Probándolo

Es muy fácil probarlo. Con el REPL de Node podemos probar partes del módulo antes de publicarlo a npm.

Para abrir el REPL ejecuta Node sin argumentos

node

E introduce línea por línea lo que quieras probar:

var md = require("./");
md.render("__Esto es Markdown__");

Verás el resultado por la pantalla:

Ahora que ya sabemos que funciona podemos publicarlo a npm si queremos con:

npm publish

Aunque recuerda revisar antes el fichero package.json para especificar la licencia y la descripción. Una vez esté publicado su uso en un nuevo proyecto será muy sencillo y de forma transparente se compilará el código nativo.

var md = require("rust-markdown");
var http = require('http');
var fs = require("fs");

var server = http.createServer((req, res) => {
  fs.readFile("index.md","utf-8",function(err,data){
     var html = md.render(data);
     res.statusCode = 200;
     res.setHeader('Content-Type', 'text/html');
     res.end(html);
  });
});

server.listen(8080, "127.0.0.1", () => {
  console.log("Server running");
});



 

 

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.