WASM: compilando Rust para o navegador

1. Introdução ao WebAssembly com Rust

WebAssembly (WASM) revolucionou o desenvolvimento web ao permitir que linguagens como Rust executem código próximo à velocidade nativa dentro do navegador. Rust se destaca nesse cenário por três razões fundamentais:

  • Desempenho: Sem garbage collector e com controle fino de memória, Rust produz binários WASM extremamente eficientes.
  • Segurança: O sistema de ownership e borrow checker elimina classes inteiras de vulnerabilidades de memória.
  • Ecossistema: Ferramentas como wasm-pack, wasm-bindgen e wasm-opt formam um pipeline maduro e produtivo.

O fluxo de trabalho típico envolve: escrever Rust → compilar para wasm32-unknown-unknown → gerar bindings JavaScript → empacotar para o navegador. Ferramentas essenciais incluem:

  • wasm-pack: Orquestra todo o processo de build, teste e publicação
  • wasm-bindgen: Gera bridges entre Rust e JavaScript
  • wasm-opt: Otimiza o binário WASM final

2. Configuração do ambiente e primeiro projeto

Primeiro, instale o target WASM:

rustup target add wasm32-unknown-unknown

Instale o wasm-pack:

cargo install wasm-pack

Crie um novo projeto:

wasm-pack new hello-wasm
cd hello-wasm

A estrutura gerada inclui:

hello-wasm/
├── Cargo.toml
├── src/
│   ├── lib.rs
│   └── utils.rs
├── pkg/          (gerado após build)
└── tests/

Configure o Cargo.toml para WASM:

[package]
name = "hello-wasm"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]

[dependencies]
wasm-bindgen = "0.2"

[profile.release]
opt-level = "s"   # Otimizar para tamanho
lto = true

3. Fundamentos do wasm-bindgen

wasm-bindgen é a peça central da interoperabilidade. Ele permite importar funções JavaScript e exportar funções Rust.

Exportando funções Rust para JavaScript

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u32 {
    match n {
        0 => 0,
        1 => 1,
        _ => fibonacci(n - 1) + fibonacci(n - 2),
    }
}

#[wasm_bindgen]
pub fn greet(name: &str) -> String {
    format!("Olá, {}! Bem-vindo ao WASM com Rust!", name)
}

Importando funções JavaScript

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);

    fn alert(s: &str);
}

#[wasm_bindgen]
pub fn show_message() {
    log("Função Rust chamada!");
    alert("Isso veio do Rust!");
}

Manipulação de tipos complexos

use wasm_bindgen::JsValue;

#[wasm_bindgen]
pub fn process_data(data: &[u8]) -> Vec<u8> {
    data.iter().map(|b| b.wrapping_add(1)).collect()
}

#[wasm_bindgen]
pub fn create_object() -> JsValue {
    let obj = js_sys::Object::new();
    js_sys::Reflect::set(&obj, &"name".into(), &"Rust WASM".into()).unwrap();
    obj
}

4. Interação com o DOM e Web APIs

A crate web-sys fornece bindings para praticamente todas as APIs do navegador.

Adicione ao Cargo.toml:

[dependencies]
web-sys = { version = "0.3", features = [
    "Document",
    "Element",
    "HtmlElement",
    "Window",
    "console",
    "CanvasRenderingContext2d",
    "HtmlCanvasElement",
] }

Manipulando o DOM

use wasm_bindgen::prelude::*;
use web_sys::{window, document, HtmlElement};

#[wasm_bindgen]
pub fn change_dom() {
    let document = document().unwrap();
    let body = document.body().unwrap();

    let div = document.create_element("div").unwrap();
    div.set_inner_html("<h1>Rust WASM no DOM!</h1>");
    body.append_child(&div).unwrap();
}

#[wasm_bindgen]
pub fn setup_click_handler() {
    let document = document().unwrap();
    let button = document.create_element("button").unwrap();
    button.set_inner_html("Clique em Rust");

    let closure = Closure::wrap(Box::new(move || {
        let window = window().unwrap();
        window.alert_with_message("Botão Rust clicado!").unwrap();
    }) as Box<dyn Fn()>);

    button
        .add_event_listener_with_callback("click", closure.as_ref().unchecked_ref())
        .unwrap();
    closure.forget();

    document.body().unwrap().append_child(&button).unwrap();
}

Timers e console.log

use wasm_bindgen::prelude::*;
use web_sys::console;

#[wasm_bindgen]
pub fn start_timer() {
    let closure = Closure::wrap(Box::new(move || {
        console::log_1(&"Tick do Rust!".into());
    }) as Box<dyn Fn()>);

    window()
        .unwrap()
        .set_interval_with_callback_and_timeout_and_arguments_0(
            closure.as_ref().unchecked_ref(),
            1000,
        )
        .unwrap();
    closure.forget();
}

5. Performance e otimizações

Usando wee_alloc para reduzir alocações

Adicione ao Cargo.toml:

[dependencies]
wee_alloc = { version = "0.4", features = ["size_classes"] }

[features]
default = ["wee_alloc"]

[profile.release]
opt-level = "z"  # Otimizar agressivamente para tamanho

No lib.rs:

#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

Compilação otimizada

# Build de produção
wasm-pack build --release

# Otimização adicional com wasm-opt
wasm-opt -Oz pkg/hello_wasm_bg.wasm -o pkg/hello_wasm_opt.wasm

Medindo o binário

# Verificar tamanho
ls -lh pkg/*.wasm

# Usar twiggy para análise
cargo install twiggy
twiggy top pkg/hello_wasm_bg.wasm

6. Exemplo prático: Canvas 2D com animação

Implementando um visualizador de partículas:

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{CanvasRenderingContext2d, HtmlCanvasElement, window};

struct Particle {
    x: f64,
    y: f64,
    vx: f64,
    vy: f64,
    radius: f64,
    color: String,
}

#[wasm_bindgen]
pub fn start_particles(canvas_id: &str) {
    let document = web_sys::document().unwrap();
    let canvas = document
        .get_element_by_id(canvas_id)
        .unwrap()
        .dyn_into::<HtmlCanvasElement>()
        .unwrap();

    let context = canvas
        .get_context("2d")
        .unwrap()
        .unwrap()
        .dyn_into::<CanvasRenderingContext2d>()
        .unwrap();

    let mut particles: Vec<Particle> = (0..100)
        .map(|_| Particle {
            x: js_sys::Math::random() * canvas.width() as f64,
            y: js_sys::Math::random() * canvas.height() as f64,
            vx: (js_sys::Math::random() - 0.5) * 4.0,
            vy: (js_sys::Math::random() - 0.5) * 4.0,
            radius: js_sys::Math::random() * 5.0 + 2.0,
            color: format!(
                "hsl({}, 70%, 50%)",
                (js_sys::Math::random() * 360.0) as u32
            ),
        })
        .collect();

    let f: Rc<RefCell<Option<Closure<dyn Fn()>>>> = Rc::new(RefCell::new(None));
    let g = f.clone();

    *g.borrow_mut() = Some(Closure::wrap(Box::new(move || {
        context.clear_rect(
            0.0,
            0.0,
            canvas.width() as f64,
            canvas.height() as f64,
        );

        for particle in &mut particles {
            particle.x += particle.vx;
            particle.y += particle.vy;

            if particle.x < 0.0 || particle.x > canvas.width() as f64 {
                particle.vx = -particle.vx;
            }
            if particle.y < 0.0 || particle.y > canvas.height() as f64 {
                particle.vy = -particle.vy;
            }

            context.set_fill_style(&particle.color.as_str().into());
            context.begin_path();
            context
                .arc(particle.x, particle.y, particle.radius, 0.0, 
                     std::f64::consts::PI * 2.0)
                .unwrap();
            context.fill();
        }

        window()
            .unwrap()
            .request_animation_frame(f.borrow().as_ref().unwrap().as_ref().unchecked_ref())
            .unwrap();
    }) as Box<dyn Fn()>));

    window()
        .unwrap()
        .request_animation_frame(g.borrow().as_ref().unwrap().as_ref().unchecked_ref())
        .unwrap();
}

7. Publicação e integração com ferramentas modernas

Integração com Webpack

Instale o plugin:

npm install --save-dev @wasm-tool/wasm-pack-plugin

Configure no webpack.config.js:

const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");
const path = require("path");

module.exports = {
    entry: "./index.js",
    output: {
        path: path.resolve(__dirname, "dist"),
        filename: "bundle.js",
    },
    plugins: [
        new WasmPackPlugin({
            crateDirectory: path.resolve(__dirname, "."),
            outDir: path.resolve(__dirname, "pkg"),
        }),
    ],
    experiments: {
        asyncWebAssembly: true,
    },
};

Publicando no npm

wasm-pack build --release --scope meu-usuario
wasm-pack publish

Considerações de CSP

Para usar WASM com Content Security Policy, adicione:

<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; script-src 'self' 'unsafe-eval';">

Ou, mais restritivamente:

<meta http-equiv="Content-Security-Policy" 
      content="default-src 'self'; script-src 'self' 'wasm-unsafe-eval';">

WebAssembly com Rust oferece um caminho sólido para levar código de alto desempenho ao navegador. Com ferramentas maduras como wasm-pack e um ecossistema crescente de crates como web-sys e js-sys, você pode construir desde pequenos componentes interativos até aplicações completas que rodam com eficiência próxima à nativa. O futuro do desenvolvimento web certamente passará por WASM, e Rust está na vanguarda dessa revolução.

Referências