Desenvolvimento

13 abr, 2017

Guia definitivo para Rust, SDL 2 e Emscripten!

Publicidade

Então, você quer escrever um jogo? E executá-lo no navegador? Gosta de Rust? Ótimo! Vou mostrar-lhe como escrever um programa de amostra em Rust usando SDL2 e como compilá-lo com Emscripten.

Instalando Rust

A melhor maneira de instalar Rust começa aqui: https://www.rustup.rs/.

$ curl https://sh.rustup.rs -sSf | sh

Em seguida, basta digitar:

$ rustup default stable

Também precisará de um alvo adicional para compilar nosso programa para JavaScript.

$ rustup default stable

Agora temos Rust!

Instalando Emscripten

Nós também precisamos do Emscripten. É um compilador que pega qualquer linguagem compatível LLVM e produz um javascript.

Existe uma ferramenta que irá gerenciar a instalação do Emscripten para nós.

$ git clone https://github.com/juj/emsdk
$ cd emsdk
$ ./emsdk install latest
$ ./emsdk activate latest

Para configurar o ambiente emscripten, basta executar:

source ./emsdk_env.sh

Configurando o SDL2

Você terá que instalar o SDL2 em seu sistema.

No MacOS:

$ brew install sdl2

No Ubuntu:

$ apt-get install libsdl2–2.0–0 libsdl2-dev

Em outras distribuições Linux, tente algo semelhante.

Criando um novo projeto Rust

Cargo irá criar um novo projeto para você, por exemplo:

$ cargo new — bin rust_to_js
$ cd rust_to_js
$ cargo run

Nós temos isso. Hello world ativo e em funcionamento.

Precisamos adicionar essas dependências ao Cargo.toml.

[dependencies]
sdl2 = “0.29.0”

E modificar src/main.rs desta forma:

extern crate sdl2;

use std::process;
use sdl2::rect::{Rect};
use sdl2::event::{Event};
use sdl2::keyboard::Keycode;

fn main() {
    let ctx = sdl2::init().unwrap();
    let video_ctx = ctx.video().unwrap();

    let window  = match video_ctx
        .window("rust_to_js", 640, 480)
        .position_centered()
        .opengl()
        .build() {
            Ok(window) => window,
            Err(err)   => panic!("failed to create window: {}", err)
        };

    let mut renderer = match window
        .renderer()
        .build() {
            Ok(renderer) => renderer,
            Err(err) => panic!("failed to create renderer: {}", err)
        };

    let mut rect = Rect::new(10, 10, 10, 10);
    
    let black = sdl2::pixels::Color::RGB(0, 0, 0);
    let white = sdl2::pixels::Color::RGB(255, 255, 255);

    let mut events = ctx.event_pump().unwrap();

    let mut main_loop = || {
        for event in events.poll_iter() {
            match event {
                Event::Quit {..} | Event::KeyDown {keycode: Some(Keycode::Escape), ..} => {
                    process::exit(1);
                },
                Event::KeyDown { keycode: Some(Keycode::Left), ..} => {
                    rect.x -= 10;
                },
                Event::KeyDown { keycode: Some(Keycode::Right), ..} => {
                    rect.x += 10;
                },
                Event::KeyDown { keycode: Some(Keycode::Up), ..} => {
                    rect.y -= 10;
                },
                Event::KeyDown { keycode: Some(Keycode::Down), ..} => {
                    rect.y += 10;
                },
                _ => {}
            }
        }

        let _ = renderer.set_draw_color(black);
        let _ = renderer.clear();
        let _ = renderer.set_draw_color(white);
        let _ = renderer.fill_rect(rect);
        let _ = renderer.present();
    };

    loop { main_loop(); }
}

Algumas explicações. Neste exemplo, criamos um aplicativo de amostra para mover uma caixa branca usando as teclas de cursor. Simples o suficiente.

Execute-o!

$ cargo run

Ótimo! Uma janela apareceu e a caixa está se movendo.

Se você vir note: ld: library not found for -lSDL2, você tem que verificar se SLD2 está instalado corretamente em seu sistema.

Tempo para modificar o nosso código para executar também em javascript.

Vamos precisar de um invólucro emscripten API. Não há nenhuma solução de trabalho no Crate (gerenciador de pacotes para Rust), mas podemos encontrar algumas na Internet.

Salve esse arquivo para src/emscripten.rs.

// taken from https://github.com/Gigoteur/PX8/blob/master/src/px8/emscripten.rs

#[cfg(target_os = "emscripten")]
pub mod emscripten {
    use std::cell::RefCell;
    use std::ptr::null_mut;
    use std::os::raw::{c_int, c_void, c_float};

    #[allow(non_camel_case_types)]
    type em_callback_func = unsafe extern fn();

    extern {
        pub fn emscripten_set_main_loop(func: em_callback_func, fps: c_int, simulate_infinite_loop: c_int);
        pub fn emscripten_cancel_main_loop();
        pub fn emscripten_get_now() -> c_float;
    }

    thread_local!(static MAIN_LOOP_CALLBACK: RefCell<*mut c_void> = RefCell::new(null_mut()));

    pub fn set_main_loop_callback<F>(callback: F) where F: FnMut() {
        MAIN_LOOP_CALLBACK.with(|log| {
            *log.borrow_mut() = &callback as *const _ as *mut c_void;
        });

        unsafe { emscripten_set_main_loop(wrapper::<F>, 0, 1); }

        unsafe extern "C" fn wrapper<F>() where F: FnMut() {
            MAIN_LOOP_CALLBACK.with(|z| {
                let closure = *z.borrow_mut() as *mut F;
                (*closure)();
            });
        }
    }
}

E modifique nosso src/main.rs para que fique assim:

extern crate sdl2;

use std::process;
use sdl2::rect::{Rect};
use sdl2::event::{Event};
use sdl2::keyboard::Keycode;

#[cfg(target_os = "emscripten")]
pub mod emscripten;

fn main() {
    let ctx = sdl2::init().unwrap();
    let video_ctx = ctx.video().unwrap();

    let window  = match video_ctx
        .window("rust_to_js", 640, 480)
        .position_centered()
        .opengl()
        .build() {
            Ok(window) => window,
            Err(err)   => panic!("failed to create window: {}", err)
        };

    let mut renderer = match window
        .renderer()
        .build() {
            Ok(renderer) => renderer,
            Err(err) => panic!("failed to create renderer: {}", err)
        };

    let mut rect = Rect::new(10, 10, 10, 10);
    
    let black = sdl2::pixels::Color::RGB(0, 0, 0);
    let white = sdl2::pixels::Color::RGB(255, 255, 255);
    
    let mut events = ctx.event_pump().unwrap();

    let mut main_loop = || {
        for event in events.poll_iter() {
            match event {
                Event::Quit {..} | Event::KeyDown {keycode: Some(Keycode::Escape), ..} => {
                    process::exit(1);
                },
                Event::KeyDown { keycode: Some(Keycode::Left), ..} => {
                    rect.x -= 10;
                },
                Event::KeyDown { keycode: Some(Keycode::Right), ..} => {
                    rect.x += 10;
                },
                Event::KeyDown { keycode: Some(Keycode::Up), ..} => {
                    rect.y -= 10;
                },
                Event::KeyDown { keycode: Some(Keycode::Down), ..} => {
                    rect.y += 10;
                },
                _ => {}
            }
        }

        let _ = renderer.set_draw_color(black);
        let _ = renderer.clear();
        let _ = renderer.set_draw_color(white);
        let _ = renderer.fill_rect(rect);
        let _ = renderer.present();
    };

    #[cfg(target_os = "emscripten")]
    use emscripten::{emscripten};

    #[cfg(target_os = "emscripten")]
    emscripten::set_main_loop_callback(main_loop);

    #[cfg(not(target_os = "emscripten"))]
    loop { main_loop(); }
}

Precisamos alternar o nosso código quando estamos compilando para javascript. Usaremos #[cfg(target_os = “emscripten”)] para definir linhas que serão usadas para construir javascript e #[cfg(não(target_os = “emscripten”))] para construções padrão.

Apenas para ter certeza, vamos verificar se todas as coisas estão funcionando para construções padrão?

$ cargo run

Sim! Tudo funciona bem.

Compilando para JavaScript

Se queremos compilar algo para javascript que use bibliotecas de sistema, precisamos compilar essas bibliotecas com emscripten. Mas emscripten nos fornece poucas bibliotecas pré-compiladas. Você pode verificá-las com:

$ emcc --show-ports

Há SDL, ótimo! Não precisamos compilá-lo nós mesmos. Precisamos usar USE_SDL=2 porque estamos usando a versão 2.0.

Antes, de adicionar quaisquer flags de compilador empscripten ao nosso ambiente, vamos compilar nossas dependências.

$ embuilder.py build sdl2

Precisamos definir a variável de ambiente EMMAKEN_CFLAGS para -s USE_SDL=2.

$ export EMMAKEN_CFLAGS="-s USE_SDL=2"

Agora vamos tentar compilar:

$ cargo build — target asmjs-unknown-emscripten

Consegui! Dê uma olhada:

$ ls target/asmjs-unknown-emscripten/debug/rust_to_js.js

Um arquivo javascript grande. É isso aí? Como executá-lo?

Você tem que criar outro arquivo index.html:

<!doctype html>
<html lang="en-us">
  <head>
    <meta charset="utf-8">
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8">
    <title>Emscripten-Generated Code</title>
    <style>
      body {
        font-family: arial;
        margin: 0;
        padding: none;
      }
      .emscripten { padding-right: 0; margin-left: auto; margin-right: auto; display: block; }
      div.emscripten { text-align: center; }
      div.emscripten_border { border: 1px solid black; }
      /* the canvas *must not* have any border or padding, or mouse coords will be wrong */
      canvas.emscripten { border: 0px none; background-color: black; }
      #emscripten_logo {
        display: inline-block;
        margin: 0;
      }
      .spinner {
        height: 30px;
        width: 30px;
        margin: 0;
        margin-top: 20px;
        margin-left: 20px;
        display: inline-block;
        vertical-align: top;
        -webkit-animation: rotation .8s linear infinite;
        -moz-animation: rotation .8s linear infinite;
        -o-animation: rotation .8s linear infinite;
        animation: rotation 0.8s linear infinite;
        border-left: 5px solid rgb(235, 235, 235);
        border-right: 5px solid rgb(235, 235, 235);
        border-bottom: 5px solid rgb(235, 235, 235);
        border-top: 5px solid rgb(120, 120, 120);
        border-radius: 100%;
        background-color: rgb(189, 215, 46);
      }
      @-webkit-keyframes rotation {
        from {-webkit-transform: rotate(0deg);}
        to {-webkit-transform: rotate(360deg);}
      }
      @-moz-keyframes rotation {
        from {-moz-transform: rotate(0deg);}
        to {-moz-transform: rotate(360deg);}
      }
      @-o-keyframes rotation {
        from {-o-transform: rotate(0deg);}
        to {-o-transform: rotate(360deg);}
      }
      @keyframes rotation {
        from {transform: rotate(0deg);}
        to {transform: rotate(360deg);}
      }
      #status {
        display: inline-block;
        vertical-align: top;
        margin-top: 30px;
        margin-left: 20px;
        font-weight: bold;
        color: rgb(120, 120, 120);
      }
      #progress {
        height: 20px;
        width: 30px;
      }
      #controls {
        display: inline-block;
        float: right;
        vertical-align: top;
        margin-top: 30px;
        margin-right: 20px;
      }
      #output {
        width: 100%;
        height: 200px;
        margin: 0 auto;
        margin-top: 10px;
        border-left: 0px;
        border-right: 0px;
        padding-left: 0px;
        padding-right: 0px;
        display: block;
        background-color: black;
        color: white;
        font-family: 'Lucida Console', Monaco, monospace;
        outline: none;
      }
    </style>
  </head>
  <body>
    <div class="spinner" id='spinner'></div>
    <div class="emscripten" id="status">Downloading...</div>

    <span id='controls'>
      <span><input type="checkbox" id="resize">Resize canvas</span>
      <span><input type="checkbox" id="pointerLock" checked>Lock/hide mouse pointer    </span>
      <span><input type="button" value="Fullscreen" onclick="Module.requestFullscreen(document.getElementById('pointerLock').checked,
                                                                                document.getElementById('resize').checked)">
      </span>
    </span>

    <div class="emscripten">
      <progress value="0" max="100" id="progress" hidden=1></progress>
    </div>

    <div class="emscripten_border">
      <canvas class="emscripten" id="canvas" oncontextmenu="event.preventDefault()"></canvas>
    </div>
    <textarea id="output" rows="8"></textarea>

    <script type='text/javascript'>
      var statusElement = document.getElementById('status');
      var progressElement = document.getElementById('progress');
      var spinnerElement = document.getElementById('spinner');
      var Module = {
        preRun: [],
        postRun: [],
        print: (function() {
          var element = document.getElementById('output');
          if (element) element.value = ''; // clear browser cache
          return function(text) {
            if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
            // These replacements are necessary if you render to raw HTML
            //text = text.replace(/&/g, "&amp;");
            //text = text.replace(/</g, "&lt;");
            //text = text.replace(/>/g, "&gt;");
            //text = text.replace('\n', '<br>', 'g');
            console.log(text);
            if (element) {
              element.value += text + "\n";
              element.scrollTop = element.scrollHeight; // focus on bottom
            }
          };
        })(),
        printErr: function(text) {
          if (arguments.length > 1) text = Array.prototype.slice.call(arguments).join(' ');
          if (0) { // XXX disabled for safety typeof dump == 'function') {
            dump(text + '\n'); // fast, straight to the real console
          } else {
            console.error(text);
          }
        },
        canvas: (function() {
          var canvas = document.getElementById('canvas');
          // As a default initial behavior, pop up an alert when webgl context is lost. To make your
          // application robust, you may want to override this behavior before shipping!
          // See http://www.khronos.org/registry/webgl/specs/latest/1.0/#5.15.2
          canvas.addEventListener("webglcontextlost", function(e) { alert('WebGL context lost. You will need to reload the page.'); e.preventDefault(); }, false);
          return canvas;
        })(),
        setStatus: function(text) {
          if (!Module.setStatus.last) Module.setStatus.last = { time: Date.now(), text: '' };
          if (text === Module.setStatus.text) return;
          var m = text.match(/([^(]+)\((\d+(\.\d+)?)\/(\d+)\)/);
          var now = Date.now();
          if (m && now - Date.now() < 30) return; // if this is a progress update, skip it if too soon
          if (m) {
            text = m[1];
            progressElement.value = parseInt(m[2])*100;
            progressElement.max = parseInt(m[4])*100;
            progressElement.hidden = false;
            spinnerElement.hidden = false;
          } else {
            progressElement.value = null;
            progressElement.max = null;
            progressElement.hidden = true;
            if (!text) spinnerElement.style.display = 'none';
          }
          statusElement.innerHTML = text;
        },
        totalDependencies: 0,
        monitorRunDependencies: function(left) {
          this.totalDependencies = Math.max(this.totalDependencies, left);
          Module.setStatus(left ? 'Preparing... (' + (this.totalDependencies-left) + '/' + this.totalDependencies + ')' : 'All downloads complete.');
        }
      };
      Module.setStatus('Downloading...');
      window.onerror = function(event) {
        // TODO: do not warn on ok events like simulating an infinite loop or exitStatus
        Module.setStatus('Exception thrown, see JavaScript console');
        spinnerElement.style.display = 'none';
        Module.setStatus = function(text) {
          if (text) Module.printErr('[post-exception status] ' + text);
        };
      };
    </script>
    <script type="text/javascript" src="target/asmjs-unknown-emscripten/debug/rust_to_js.js"></script>
  </body>
</html>

Isso é um wrapper que irá baixar código javascript gerado e executá-lo no navegador. Olhe para a linha 190, existe um link para esse arquivo javascript.

Vamos abrir o index.html no navegador.

Funciona! Você pode usar os botões do cursor para mover a pequena caixa branca.

Como você pode ver, é bastante fácil gerar javascript que funcione no código Rust e executá-lo no navegador. Agora, você deveria criar algum programa mais interessante ou mesmo um jogo!

Aqui está um repositório GitHub com todo o código que acabamos de criar. Espero que goste!

***

Michał Kalbarczyk faz parte do time de colunistas internacionais do iMasters. A tradução do artigo é feita pela redação iMasters, com autorização do autor, e você pode acompanhar o artigo em inglês no link: https://blog.fazibear.me/definitive-guide-to-rust-sdl-2-and-emscripten-93d707b22bbb.