WebAssembly com Rust: Tutorial Completo | Rust Brasil

Tutorial de WebAssembly com Rust: wasm-pack, wasm-bindgen, Yew e deploy no navegador. Guia passo a passo em português.

Introdução

WebAssembly (WASM) é um formato binário de instruções que roda em navegadores modernos com performance próxima ao código nativo. Rust é uma das linguagens com melhor suporte para WebAssembly, graças a ferramentas como wasm-pack e wasm-bindgen. Neste tutorial, vamos explorar como compilar Rust para WASM, integrar com JavaScript e construir aplicações web completas.

Por Que Rust para WebAssembly?

  • Performance: código Rust compilado para WASM é extremamente rápido
  • Tamanho pequeno: binários WASM em Rust são compactos
  • Segurança de memória: sem garbage collector, sem vazamentos
  • Ecossistema maduro: ferramentas como wasm-pack simplificam o workflow
  • Interop com JS: wasm-bindgen facilita a comunicação entre Rust e JavaScript

Configurando o Ambiente

Instalando as Ferramentas

# Adicionar o target wasm32
rustup target add wasm32-unknown-unknown

# Instalar wasm-pack
cargo install wasm-pack

# Opcional: instalar cargo-generate para templates
cargo install cargo-generate

Criando o Projeto

cargo new --lib wasm-demo
cd wasm-demo

Configure o Cargo.toml:

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

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

[dependencies]
wasm-bindgen = "0.2"
js-sys = "0.3"
web-sys = { version = "0.3", features = [
    "console",
    "Document",
    "Element",
    "HtmlElement",
    "HtmlCanvasElement",
    "CanvasRenderingContext2d",
    "Window",
    "Node",
] }
serde = { version = "1", features = ["derive"] }
serde-wasm-bindgen = "0.6"

wasm-bindgen: A Ponte entre Rust e JavaScript

O wasm-bindgen gera automaticamente o código de ligação (binding) entre Rust e JavaScript.

Exportando Funções para JavaScript

// src/lib.rs
use wasm_bindgen::prelude::*;

// Função exportada para JavaScript
#[wasm_bindgen]
pub fn saudacao(nome: &str) -> String {
    format!("Olá, {}! Bem-vindo ao WebAssembly com Rust!", nome)
}

#[wasm_bindgen]
pub fn fibonacci(n: u32) -> u64 {
    match n {
        0 => 0,
        1 => 1,
        _ => {
            let mut a: u64 = 0;
            let mut b: u64 = 1;
            for _ in 2..=n {
                let temp = b;
                b = a + b;
                a = temp;
            }
            b
        }
    }
}

#[wasm_bindgen]
pub fn ordenar_numeros(mut numeros: Vec<f64>) -> Vec<f64> {
    numeros.sort_by(|a, b| a.partial_cmp(b).unwrap());
    numeros
}

Chamando JavaScript a partir do Rust

use wasm_bindgen::prelude::*;

// Importando funções JavaScript
#[wasm_bindgen]
extern "C" {
    // Importar alert do navegador
    fn alert(s: &str);

    // Importar console.log
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);

    // Importar Math.random
    #[wasm_bindgen(js_namespace = Math)]
    fn random() -> f64;
}

// Macro para facilitar o logging
macro_rules! console_log {
    ($($t:tt)*) => (log(&format!($($t)*)))
}

#[wasm_bindgen]
pub fn demonstracao() {
    console_log!("Rust rodando no navegador via WASM!");

    let numero_aleatorio = random();
    console_log!("Número aleatório do JS: {}", numero_aleatorio);

    alert("Esta mensagem veio do Rust!");
}

Trabalhando com Structs

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct Calculadora {
    historico: Vec<String>,
    resultado: f64,
}

#[wasm_bindgen]
impl Calculadora {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Calculadora {
        Calculadora {
            historico: Vec::new(),
            resultado: 0.0,
        }
    }

    pub fn somar(&mut self, valor: f64) -> f64 {
        self.resultado += valor;
        self.historico.push(format!("+ {}", valor));
        self.resultado
    }

    pub fn subtrair(&mut self, valor: f64) -> f64 {
        self.resultado -= valor;
        self.historico.push(format!("- {}", valor));
        self.resultado
    }

    pub fn multiplicar(&mut self, valor: f64) -> f64 {
        self.resultado *= valor;
        self.historico.push(format!("* {}", valor));
        self.resultado
    }

    pub fn dividir(&mut self, valor: f64) -> Result<f64, JsError> {
        if valor == 0.0 {
            return Err(JsError::new("Divisão por zero não é permitida"));
        }
        self.resultado /= valor;
        self.historico.push(format!("/ {}", valor));
        Ok(self.resultado)
    }

    pub fn resultado(&self) -> f64 {
        self.resultado
    }

    pub fn limpar(&mut self) {
        self.resultado = 0.0;
        self.historico.clear();
    }

    pub fn historico(&self) -> String {
        self.historico.join(", ")
    }
}

Manipulando o DOM

Com web-sys, podemos manipular o DOM diretamente do Rust:

use wasm_bindgen::prelude::*;
use web_sys::{console, Document, Element, HtmlElement, Window};

fn window() -> Window {
    web_sys::window().expect("Sem objeto window global")
}

fn document() -> Document {
    window().document().expect("Sem document no window")
}

#[wasm_bindgen]
pub fn criar_lista_tarefas() -> Result<(), JsValue> {
    let document = document();

    // Criar container
    let container = document.create_element("div")?;
    container.set_id("app-tarefas");

    // Criar título
    let titulo = document.create_element("h2")?;
    titulo.set_text_content(Some("Lista de Tarefas (Rust + WASM)"));
    container.append_child(&titulo)?;

    // Criar campo de input
    let input = document.create_element("input")?;
    input.set_attribute("type", "text")?;
    input.set_attribute("id", "nova-tarefa")?;
    input.set_attribute("placeholder", "Digite uma nova tarefa...")?;
    container.append_child(&input)?;

    // Criar botão
    let botao = document
        .create_element("button")?
        .dyn_into::<HtmlElement>()?;
    botao.set_text_content(Some("Adicionar"));

    // Criar lista
    let lista = document.create_element("ul")?;
    lista.set_id("lista-tarefas");
    container.append_child(&lista)?;

    // Adicionar evento ao botão
    let closure = Closure::wrap(Box::new(move || {
        adicionar_tarefa();
    }) as Box<dyn Fn()>);

    botao.set_onclick(Some(closure.as_ref().unchecked_ref()));
    closure.forget(); // Mantém o closure vivo

    container.append_child(&botao)?;

    // Adicionar ao body
    let body = document.body().expect("Sem body no document");
    body.append_child(&container)?;

    console::log_1(&"Lista de tarefas criada com sucesso!".into());

    Ok(())
}

fn adicionar_tarefa() {
    let document = document();

    if let Some(input) = document.get_element_by_id("nova-tarefa") {
        let input: web_sys::HtmlInputElement = input.dyn_into().unwrap();
        let texto = input.value();

        if !texto.trim().is_empty() {
            if let Some(lista) = document.get_element_by_id("lista-tarefas") {
                let item = document.create_element("li").unwrap();
                item.set_text_content(Some(&texto));

                // Adicionar estilo e evento de clique para marcar como concluída
                let item_clone = item.clone();
                let closure = Closure::wrap(Box::new(move || {
                    let elem: &HtmlElement = item_clone.dyn_ref().unwrap();
                    let estilo = elem.style();
                    let _ = estilo.set_property("text-decoration", "line-through");
                    let _ = estilo.set_property("color", "#888");
                }) as Box<dyn Fn()>);

                let html_item: &HtmlElement = item.dyn_ref().unwrap();
                html_item.set_onclick(Some(closure.as_ref().unchecked_ref()));
                closure.forget();

                lista.append_child(&item).unwrap();
                input.set_value(""); // Limpar input
            }
        }
    }
}

Compilando e Usando no Navegador

Compilar com wasm-pack

# Compilar para uso com bundlers (webpack, vite)
wasm-pack build --target bundler

# Compilar para uso direto na web (sem bundler)
wasm-pack build --target web

# Compilar para Node.js
wasm-pack build --target nodejs

Integração Direta na Web (sem bundler)

Crie um arquivo index.html:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Rust + WebAssembly Demo</title>
    <style>
        body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
        input { padding: 8px; margin-right: 8px; }
        button { padding: 8px 16px; cursor: pointer; }
        #resultado { margin-top: 20px; padding: 10px; background: #f0f0f0; }
    </style>
</head>
<body>
    <h1>Rust + WebAssembly</h1>

    <div>
        <input type="text" id="nome" placeholder="Seu nome">
        <button onclick="saudar()">Saudar</button>
    </div>

    <div>
        <input type="number" id="fib-n" placeholder="N para Fibonacci">
        <button onclick="calcularFib()">Calcular Fibonacci</button>
    </div>

    <div id="resultado"></div>

    <script type="module">
        import init, { saudacao, fibonacci, criar_lista_tarefas, Calculadora } from './pkg/wasm_demo.js';

        async function main() {
            await init();

            // Disponibilizar funções globalmente
            window.saudar = function() {
                const nome = document.getElementById('nome').value;
                const msg = saudacao(nome || 'Mundo');
                document.getElementById('resultado').textContent = msg;
            };

            window.calcularFib = function() {
                const n = parseInt(document.getElementById('fib-n').value) || 10;
                const inicio = performance.now();
                const resultado = fibonacci(n);
                const tempo = (performance.now() - inicio).toFixed(2);
                document.getElementById('resultado').textContent =
                    `Fibonacci(${n}) = ${resultado} (calculado em ${tempo}ms)`;
            };

            // Criar lista de tarefas
            criar_lista_tarefas();

            // Demonstrar a calculadora
            const calc = new Calculadora();
            calc.somar(10);
            calc.multiplicar(5);
            calc.subtrair(3);
            console.log('Resultado:', calc.resultado());
            console.log('Histórico:', calc.historico());
        }

        main();
    </script>
</body>
</html>

Servindo Localmente

# Instalar um servidor HTTP simples
cargo install miniserve

# Servir o projeto (da raiz do projeto)
miniserve . --index index.html -p 8080

Desenhando no Canvas com WASM

use wasm_bindgen::prelude::*;
use web_sys::CanvasRenderingContext2d;
use std::f64::consts::PI;

#[wasm_bindgen]
pub fn desenhar_grafico(canvas_id: &str) -> Result<(), JsValue> {
    let document = web_sys::window().unwrap().document().unwrap();
    let canvas = document
        .get_element_by_id(canvas_id)
        .unwrap()
        .dyn_into::<web_sys::HtmlCanvasElement>()?;

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

    let largura = canvas.width() as f64;
    let altura = canvas.height() as f64;

    // Limpar canvas
    ctx.clear_rect(0.0, 0.0, largura, altura);

    // Desenhar fundo
    ctx.set_fill_style_str("#1a1a2e");
    ctx.fill_rect(0.0, 0.0, largura, altura);

    // Desenhar onda senoidal
    ctx.begin_path();
    ctx.set_stroke_style_str("#e94560");
    ctx.set_line_width(2.0);

    let centro_y = altura / 2.0;
    let amplitude = altura / 3.0;
    let frequencia = 4.0 * PI / largura;

    ctx.move_to(0.0, centro_y);
    for x in 0..=(largura as i32) {
        let xf = x as f64;
        let y = centro_y + amplitude * (xf * frequencia).sin();
        ctx.line_to(xf, y);
    }
    ctx.stroke();

    // Desenhar segunda onda (cosseno)
    ctx.begin_path();
    ctx.set_stroke_style_str("#0f3460");
    ctx.set_line_width(2.0);

    ctx.move_to(0.0, centro_y);
    for x in 0..=(largura as i32) {
        let xf = x as f64;
        let y = centro_y + (amplitude * 0.7) * (xf * frequencia).cos();
        ctx.line_to(xf, y);
    }
    ctx.stroke();

    // Título
    ctx.set_fill_style_str("#ffffff");
    ctx.set_font("16px Arial");
    ctx.fill_text("Gráfico gerado com Rust + WASM", 10.0, 25.0)?;

    Ok(())
}

Visão Geral do Framework Yew

O Yew é um framework Rust para criar aplicações web SPA (Single Page Application) que compilam para WebAssembly. Ele utiliza um modelo baseado em componentes, similar ao React.

Configuração do Yew

[dependencies]
yew = { version = "0.21", features = ["csr"] }

Exemplo de Componente Yew

use yew::prelude::*;

#[derive(Clone, PartialEq)]
struct Tarefa {
    id: usize,
    texto: String,
    concluida: bool,
}

#[function_component(App)]
fn app() -> Html {
    let tarefas = use_state(|| Vec::<Tarefa>::new());
    let contador_id = use_state(|| 0usize);
    let input_ref = use_node_ref();

    let on_adicionar = {
        let tarefas = tarefas.clone();
        let contador_id = contador_id.clone();
        let input_ref = input_ref.clone();

        Callback::from(move |_| {
            if let Some(input) = input_ref.cast::<web_sys::HtmlInputElement>() {
                let texto = input.value();
                if !texto.trim().is_empty() {
                    let novo_id = *contador_id + 1;
                    contador_id.set(novo_id);

                    let mut novas_tarefas = (*tarefas).clone();
                    novas_tarefas.push(Tarefa {
                        id: novo_id,
                        texto,
                        concluida: false,
                    });
                    tarefas.set(novas_tarefas);
                    input.set_value("");
                }
            }
        })
    };

    let on_toggle = {
        let tarefas = tarefas.clone();
        Callback::from(move |id: usize| {
            let mut novas_tarefas = (*tarefas).clone();
            if let Some(tarefa) = novas_tarefas.iter_mut().find(|t| t.id == id) {
                tarefa.concluida = !tarefa.concluida;
            }
            tarefas.set(novas_tarefas);
        })
    };

    let on_remover = {
        let tarefas = tarefas.clone();
        Callback::from(move |id: usize| {
            let novas_tarefas: Vec<_> = (*tarefas)
                .iter()
                .filter(|t| t.id != id)
                .cloned()
                .collect();
            tarefas.set(novas_tarefas);
        })
    };

    html! {
        <div class="app">
            <h1>{ "Lista de Tarefas - Yew + WASM" }</h1>
            <div class="input-area">
                <input
                    ref={input_ref}
                    type="text"
                    placeholder="Nova tarefa..."
                />
                <button onclick={on_adicionar}>{ "Adicionar" }</button>
            </div>
            <ul>
                { for tarefas.iter().map(|tarefa| {
                    let id = tarefa.id;
                    let on_toggle = on_toggle.clone();
                    let on_remover = on_remover.clone();
                    let classe = if tarefa.concluida { "concluida" } else { "" };

                    html! {
                        <li class={classe}>
                            <span onclick={move |_| on_toggle.emit(id)}>
                                { &tarefa.texto }
                            </span>
                            <button onclick={move |_| on_remover.emit(id)}>
                                { "Remover" }
                            </button>
                        </li>
                    }
                })}
            </ul>
            <p>{ format!("Total: {} tarefas", tarefas.len()) }</p>
        </div>
    }
}

fn main() {
    yew::Renderer::<App>::new().render();
}

Compilando o Projeto Yew

Para compilar e servir um projeto Yew, use o Trunk:

# Instalar Trunk
cargo install trunk

# Criar index.html para o Trunk
# O Trunk detecta automaticamente o projeto e gera o WASM

# Servir com hot-reload
trunk serve

# Compilar para produção
trunk build --release

Crie um index.html na raiz do projeto Yew:

<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>App Yew</title>
</head>
<body></body>
</html>

Deploy para a Web

Otimizando o Tamanho do WASM

# Compilar com otimizações de tamanho
wasm-pack build --release --target web

# Usar wasm-opt para otimização adicional (instale o binaryen)
wasm-opt -Os -o pkg/wasm_demo_bg_opt.wasm pkg/wasm_demo_bg.wasm

No Cargo.toml, adicione perfis de otimização:

[profile.release]
opt-level = "s"      # Otimizar para tamanho
lto = true           # Link-time optimization
strip = true         # Remover símbolos de debug
codegen-units = 1    # Melhor otimização

Deploy em Plataformas

Os arquivos gerados em pkg/ (ou dist/ com Trunk) são estáticos e podem ser hospedados em qualquer serviço:

# Netlify
netlify deploy --prod --dir=dist

# Vercel
vercel --prod dist

# GitHub Pages
# Configure o GitHub Actions para compilar e publicar automaticamente

Conclusão

WebAssembly com Rust abre um mundo de possibilidades para desenvolvimento web:

  • wasm-bindgen e web-sys fornecem acesso completo às APIs do navegador
  • wasm-pack simplifica o processo de compilação e empacotamento
  • Yew permite construir SPAs completas em Rust
  • Performance nativa no navegador para cálculos intensivos
  • Interoperabilidade transparente com JavaScript

O ecossistema WASM em Rust está maduro e em constante evolução. Seja para otimizar partes críticas de uma aplicação web existente ou para construir aplicações inteiras em Rust, WebAssembly é uma tecnologia que vale a pena dominar.