Rust

WebAssembly com Rust — Executando Rust no Navegador Já leu

14 min de leitura

WebAssembly com Rust — Executando Rust no Navegador
Rust e WebAssembly formam uma das combinações mais empolgantes da programação moderna. WebAssembly (Wasm) é um formato binário que executa em velocidade próxima à nativa dentro de qualquer navegador moderno — sem plugins

 

Rust e WebAssembly formam uma das combinações mais empolgantes da programação moderna. WebAssembly (Wasm) é um formato binário que executa em velocidade próxima à nativa dentro de qualquer navegador moderno — sem plugins, sem instalação, com segurança sandboxed. Rust compila para Wasm de forma excepcionalmente eficiente: sem runtime, sem garbage collector, binários pequenos.

O resultado prático: algoritmos computacionalmente intensivos que seriam lentos em JavaScript podem ser escritos em Rust e chamados diretamente do navegador com performance nativa.


O que é WebAssembly

WebAssembly não é uma linguagem — é um alvo de compilação. É um formato binário compacto que navegadores e runtimes como Node.js podem executar. Qualquer linguagem que compile para Wasm pode rodar no navegador: Rust, C, C++, Go, e outras.

O Wasm não substitui JavaScript — complementa. JavaScript continua controlando o DOM, eventos, e APIs do navegador. Wasm entra onde JavaScript é lento: criptografia, processamento de imagem, simulações físicas, compiladores, codecs.


Configurando o ambiente

# Instalar o target Wasm
rustup target add wasm32-unknown-unknown

# Instalar wasm-pack — ferramenta que empacota Rust+Wasm para npm
cargo install wasm-pack

# Instalar wasm-bindgen-cli (opcional, wasm-pack já inclui)
cargo install wasm-bindgen-cli

Crie um novo projeto de biblioteca:

cargo new --lib calculadora_wasm
cd calculadora_wasm

Cargo.toml:

[package]
name = "calculadora_wasm"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib", "rlib"]
# cdylib: gera .wasm para uso no navegador
# rlib: permite testes normais com cargo test

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

[dev-dependencies]
wasm-bindgen-test = "0.3"

[profile.release]
opt-level = "s"   # otimiza para tamanho do binário
lto = true

wasm-bindgen — a ponte entre Rust e JavaScript

wasm-bindgen é a biblioteca que faz a mágica acontecer. Ela gera automaticamente o código JavaScript necessário para chamar funções Rust e vice-versa:

src/lib.rs:

use wasm_bindgen::prelude::*;

// Disponibiliza função para JavaScript
#[wasm_bindgen]
pub fn somar(a: f64, b: f64) -> f64 {
    a + b
}

#[wasm_bindgen]
pub fn fatorial(n: u32) -> u64 {
    (1..=n as u64).product()
}

#[wasm_bindgen]
pub fn e_primo(n: u64) -> bool {
    if n < 2 { return false; }
    if n == 2 { return true; }
    if n % 2 == 0 { return false; }
    let limite = (n as f64).sqrt() as u64;
    (3..=limite).step_by(2).all(|i| n % i != 0)
}

// Chamando JavaScript de dentro do Rust
#[wasm_bindgen]
extern "C" {
    // Importa console.log do JavaScript
    #[wasm_bindgen(js_namespace = console)]
    fn log(s: &str);

    // Importa alert
    fn alert(s: &str);
}

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

#[wasm_bindgen]
pub fn calcular_e_logar(n: u32) -> u64 {
    console_log!("Calculando fatorial de {n}");
    let resultado = fatorial(n);
    console_log!("Resultado: {resultado}");
    resultado
}

Compile para Wasm:

wasm-pack build --target web

Isso gera o diretório pkg/ com:

  • calculadora_wasm.wasm — o binário Wasm
  • calculadora_wasm.js — wrapper JavaScript gerado automaticamente
  • calculadora_wasm.d.ts — definições TypeScript
  • package.json — para uso como pacote npm

Structs e métodos no Wasm

Structs Rust podem ser expostas como classes JavaScript:

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub struct Vetor2D {
    pub x: f64,
    pub y: f64,
}

#[wasm_bindgen]
impl Vetor2D {
    #[wasm_bindgen(constructor)]
    pub fn novo(x: f64, y: f64) -> Self {
        Vetor2D { x, y }
    }

    pub fn magnitude(&self) -> f64 {
        (self.x * self.x + self.y * self.y).sqrt()
    }

    pub fn normalizar(&self) -> Vetor2D {
        let mag = self.magnitude();
        if mag == 0.0 {
            return Vetor2D { x: 0.0, y: 0.0 };
        }
        Vetor2D {
            x: self.x / mag,
            y: self.y / mag,
        }
    }

    pub fn adicionar(&self, outro: &Vetor2D) -> Vetor2D {
        Vetor2D {
            x: self.x + outro.x,
            y: self.y + outro.y,
        }
    }

    pub fn produto_escalar(&self, outro: &Vetor2D) -> f64 {
        self.x * outro.x + self.y * outro.y
    }

    pub fn angulo_entre(&self, outro: &Vetor2D) -> f64 {
        let cos = self.produto_escalar(outro)
            / (self.magnitude() * outro.magnitude());
        cos.acos().to_degrees()
    }

    pub fn para_string(&self) -> String {
        format!("({:.4}, {:.4})", self.x, self.y)
    }
}

Em JavaScript, isso vira:

import init, { Vetor2D } from './pkg/calculadora_wasm.js';

await init();

const v1 = new Vetor2D(3.0, 4.0);
const v2 = new Vetor2D(1.0, 0.0);

console.log(v1.magnitude());      // 5.0
console.log(v1.normalizar().para_string()); // (0.6000, 0.8000)
console.log(v1.angulo_entre(v2)); // 53.13...

// Importante: structs Wasm precisam ser liberadas manualmente
// ou use em bloco try-finally
v1.free();
v2.free();

Passando dados complexos com serde

Para passar objetos complexos entre Rust e JavaScript, use serde-wasm-bindgen:

use serde::{Deserialize, Serialize};
use wasm_bindgen::prelude::*;

#[derive(Serialize, Deserialize, Debug)]
pub struct DadosAnalise {
    pub valores: Vec<f64>,
    pub rotulo: String,
}

#[derive(Serialize, Deserialize, Debug)]
pub struct ResultadoAnalise {
    pub min: f64,
    pub max: f64,
    pub media: f64,
    pub mediana: f64,
    pub desvio_padrao: f64,
    pub outliers: Vec<f64>,
}

#[wasm_bindgen]
pub fn analisar(dados_js: JsValue) -> Result<JsValue, JsValue> {
    // Desserializa o objeto JavaScript para Rust
    let dados: DadosAnalise = serde_wasm_bindgen::from_value(dados_js)
        .map_err(|e| JsValue::from_str(&e.to_string()))?;

    if dados.valores.is_empty() {
        return Err(JsValue::from_str("Lista de valores não pode ser vazia"));
    }

    let n = dados.valores.len() as f64;
    let mut ordenados = dados.valores.clone();
    ordenados.sort_by(|a, b| a.partial_cmp(b).unwrap());

    let min    = ordenados[0];
    let max    = *ordenados.last().unwrap();
    let soma: f64 = dados.valores.iter().sum();
    let media  = soma / n;

    let mediana = if ordenados.len() % 2 == 0 {
        (ordenados[ordenados.len() / 2 - 1] + ordenados[ordenados.len() / 2]) / 2.0
    } else {
        ordenados[ordenados.len() / 2]
    };

    let variancia = dados.valores.iter()
        .map(|&x| (x - media).powi(2))
        .sum::<f64>() / n;
    let desvio_padrao = variancia.sqrt();

    // Outliers: valores além de 2 desvios padrões
    let outliers: Vec<f64> = dados.valores.iter()
        .cloned()
        .filter(|&x| (x - media).abs() > 2.0 * desvio_padrao)
        .collect();

    let resultado = ResultadoAnalise {
        min, max, media, mediana, desvio_padrao, outliers,
    };

    // Serializa o struct Rust para objeto JavaScript
    serde_wasm_bindgen::to_value(&resultado)
        .map_err(|e| JsValue::from_str(&e.to_string()))
}

Um exemplo completo: processador de imagem no navegador

Vamos criar um processador de imagem que aplica filtros usando pixels brutos:

src/lib.rs (versão completa):

use wasm_bindgen::prelude::*;

// Configura o panic hook para mensagens de erro legíveis no console
#[wasm_bindgen(start)]
pub fn inicializar() {
    console_error_panic_hook::set_once();
}

/// Converte imagem colorida para escala de cinza
/// dados: array de pixels RGBA (4 bytes por pixel)
#[wasm_bindgen]
pub fn para_cinza(dados: &mut [u8]) {
    for pixel in dados.chunks_mut(4) {
        let r = pixel[0] as f64;
        let g = pixel[1] as f64;
        let b = pixel[2] as f64;

        // Luminância perceptual
        let cinza = (0.299 * r + 0.587 * g + 0.114 * b) as u8;

        pixel[0] = cinza;
        pixel[1] = cinza;
        pixel[2] = cinza;
        // pixel[3] = alpha, não alterado
    }
}

/// Inverte as cores de uma imagem
#[wasm_bindgen]
pub fn inverter(dados: &mut [u8]) {
    for pixel in dados.chunks_mut(4) {
        pixel[0] = 255 - pixel[0];
        pixel[1] = 255 - pixel[1];
        pixel[2] = 255 - pixel[2];
    }
}

/// Ajusta brilho (-255 a +255)
#[wasm_bindgen]
pub fn ajustar_brilho(dados: &mut [u8], ajuste: i16) {
    for pixel in dados.chunks_mut(4) {
        pixel[0] = ((pixel[0] as i16 + ajuste).clamp(0, 255)) as u8;
        pixel[1] = ((pixel[1] as i16 + ajuste).clamp(0, 255)) as u8;
        pixel[2] = ((pixel[2] as i16 + ajuste).clamp(0, 255)) as u8;
    }
}

/// Aplica blur gaussiano (kernel 3x3)
#[wasm_bindgen]
pub fn blur_gaussiano(dados: &[u8], largura: u32, altura: u32) -> Vec<u8> {
    let kernel: [[f64; 3]; 3] = [
        [1.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0],
        [2.0 / 16.0, 4.0 / 16.0, 2.0 / 16.0],
        [1.0 / 16.0, 2.0 / 16.0, 1.0 / 16.0],
    ];

    let mut resultado = dados.to_vec();
    let w = largura as usize;
    let h = altura as usize;

    for y in 1..h - 1 {
        for x in 1..w - 1 {
            let mut r = 0.0f64;
            let mut g = 0.0f64;
            let mut b = 0.0f64;

            for ky in 0..3usize {
                for kx in 0..3usize {
                    let px = x + kx - 1;
                    let py = y + ky - 1;
                    let idx = (py * w + px) * 4;
                    let peso = kernel[ky][kx];

                    r += dados[idx]     as f64 * peso;
                    g += dados[idx + 1] as f64 * peso;
                    b += dados[idx + 2] as f64 * peso;
                }
            }

            let idx = (y * w + x) * 4;
            resultado[idx]     = r as u8;
            resultado[idx + 1] = g as u8;
            resultado[idx + 2] = b as u8;
        }
    }

    resultado
}

/// Detecção de bordas com filtro Sobel
#[wasm_bindgen]
pub fn detectar_bordas(dados: &[u8], largura: u32, altura: u32) -> Vec<u8> {
    let w = largura as usize;
    let h = altura as usize;

    // Converter para cinza primeiro
    let mut cinza = vec![0u8; w * h];
    for y in 0..h {
        for x in 0..w {
            let idx = (y * w + x) * 4;
            let c = (0.299 * dados[idx] as f64
                + 0.587 * dados[idx + 1] as f64
                + 0.114 * dados[idx + 2] as f64) as u8;
            cinza[y * w + x] = c;
        }
    }

    let sobel_x: [[i16; 3]; 3] = [
        [-1, 0, 1],
        [-2, 0, 2],
        [-1, 0, 1],
    ];

    let sobel_y: [[i16; 3]; 3] = [
        [-1, -2, -1],
        [ 0,  0,  0],
        [ 1,  2,  1],
    ];

    let mut resultado = vec![0u8; w * h * 4];

    for y in 1..h - 1 {
        for x in 1..w - 1 {
            let mut gx = 0i16;
            let mut gy = 0i16;

            for ky in 0..3usize {
                for kx in 0..3usize {
                    let px = x + kx - 1;
                    let py = y + ky - 1;
                    let v = cinza[py * w + px] as i16;
                    gx += v * sobel_x[ky][kx];
                    gy += v * sobel_y[ky][kx];
                }
            }

            let magnitude = ((gx * gx + gy * gy) as f64)
                .sqrt()
                .min(255.0) as u8;

            let idx = (y * w + x) * 4;
            resultado[idx]     = magnitude;
            resultado[idx + 1] = magnitude;
            resultado[idx + 2] = magnitude;
            resultado[idx + 3] = 255;
        }
    }

    resultado
}

Adicione ao Cargo.toml:

console_error_panic_hook = "0.1"

A página HTML

<!DOCTYPE html>
<html lang="pt-BR">
<head>
    <meta charset="UTF-8">
    <title>Processador de Imagem — Rust + Wasm</title>
    <style>
        body {
            font-family: sans-serif;
            max-width: 900px;
            margin: 0 auto;
            padding: 20px;
            background: #1a1a2e;
            color: #eee;
        }
        h1 { color: #e94560; }
        .controles {
            display: flex;
            gap: 10px;
            flex-wrap: wrap;
            margin: 20px 0;
        }
        button {
            background: #16213e;
            color: #e94560;
            border: 1px solid #e94560;
            padding: 8px 16px;
            cursor: pointer;
            border-radius: 4px;
            font-size: 14px;
        }
        button:hover { background: #e94560; color: white; }
        .telas {
            display: grid;
            grid-template-columns: 1fr 1fr;
            gap: 20px;
        }
        canvas {
            width: 100%;
            border: 1px solid #e94560;
        }
        label { font-size: 12px; color: #aaa; }
        .tempo { color: #4ecca3; font-size: 13px; }
    </style>
</head>
<body>
    <h1>Processador de Imagem — Rust + WebAssembly</h1>

    <input type="file" id="entrada" accept="image/*">
    <p class="tempo" id="tempo"></p>

    <div class="controles">
        <button onclick="aplicarFiltro('cinza')">Escala de Cinza</button>
        <button onclick="aplicarFiltro('inverter')">Inverter</button>
        <button onclick="aplicarFiltro('blur')">Blur Gaussiano</button>
        <button onclick="aplicarFiltro('bordas')">Detectar Bordas</button>
        <button onclick="aplicarFiltro('brilho_mais')">Brilho +50</button>
        <button onclick="aplicarFiltro('brilho_menos')">Brilho -50</button>
        <button onclick="restaurar()">Restaurar Original</button>
    </div>

    <div class="telas">
        <div>
            <label>Original</label>
            <canvas id="original"></canvas>
        </div>
        <div>
            <label>Processado (Rust + Wasm)</label>
            <canvas id="processado"></canvas>
        </div>
    </div>

    <script type="module">
        import init, {
            inicializar,
            para_cinza,
            inverter,
            ajustar_brilho,
            blur_gaussiano,
            detectar_bordas,
        } from './pkg/calculadora_wasm.js';

        // Inicializa o módulo Wasm
        await init();

        let imagemOriginal = null;

        document.getElementById('entrada').addEventListener('change', async (e) => {
            const arquivo = e.target.files[0];
            if (!arquivo) return;

            const img = new Image();
            img.src = URL.createObjectURL(arquivo);
            await new Promise(resolve => img.onload = resolve);

            const canvas = document.getElementById('original');
            canvas.width  = img.width;
            canvas.height = img.height;

            const ctx = canvas.getContext('2d');
            ctx.drawImage(img, 0, 0);

            imagemOriginal = ctx.getImageData(0, 0, img.width, img.height);
            copiarParaProcessado(imagemOriginal);
        });

        function copiarParaProcessado(imageData) {
            const canvas = document.getElementById('processado');
            canvas.width  = imageData.width;
            canvas.height = imageData.height;
            canvas.getContext('2d').putImageData(imageData, 0, 0);
        }

        window.aplicarFiltro = function(filtro) {
            if (!imagemOriginal) {
                alert('Carregue uma imagem primeiro!');
                return;
            }

            const canvas = document.getElementById('processado');
            const ctx = canvas.getContext('2d');
            const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);

            const inicio = performance.now();

            switch (filtro) {
                case 'cinza':
                    // Rust modifica o array diretamente (zero-copy!)
                    para_cinza(imageData.data);
                    break;

                case 'inverter':
                    inverter(imageData.data);
                    break;

                case 'brilho_mais':
                    ajustar_brilho(imageData.data, 50);
                    break;

                case 'brilho_menos':
                    ajustar_brilho(imageData.data, -50);
                    break;

                case 'blur': {
                    // blur_gaussiano retorna novo array
                    const novos = blur_gaussiano(
                        imageData.data,
                        canvas.width,
                        canvas.height
                    );
                    imageData.data.set(novos);
                    break;
                }

                case 'bordas': {
                    const novos = detectar_bordas(
                        imageData.data,
                        canvas.width,
                        canvas.height
                    );
                    imageData.data.set(novos);
                    break;
                }
            }

            const tempo = (performance.now() - inicio).toFixed(2);
            document.getElementById('tempo').textContent =
                `⚡ Processado em ${tempo}ms pelo Rust (Wasm)`;

            ctx.putImageData(imageData, 0, 0);
        };

        window.restaurar = function() {
            if (imagemOriginal) {
                copiarParaProcessado(imagemOriginal);
                document.getElementById('tempo').textContent = '';
            }
        };
    </script>
</body>
</html>

Usando com Node.js e bundlers

Para usar o pacote gerado em um projeto Node.js ou com bundlers como Vite e webpack:

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

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

Com Vite:

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
    // Vite suporta Wasm nativamente com plugin
    plugins: [],
    optimizeDeps: {
        exclude: ['calculadora_wasm'],
    },
});
// main.js
import init, { somar, e_primo } from 'calculadora_wasm';

await init();

console.log(somar(3, 4));   // 7
console.log(e_primo(97));   // true

// Calcular todos os primos até 10000 — intensivo, mas rápido no Wasm
const primos = [];
for (let i = 2; i <= 10000; i++) {
    if (e_primo(BigInt(i))) primos.push(i);
}
console.log(`${primos.length} primos encontrados`);

Performance: Wasm vs JavaScript

O ganho de performance de Wasm sobre JavaScript varia muito por caso de uso. Para operações simples como soma de dois números, JavaScript é tão rápido quanto Wasm — o overhead de crossing a fronteira JS-Wasm domina. Para operações intensivas em dados, Wasm brilha:

// Benchmark: blur em imagem 1920x1080

// JavaScript puro: ~180ms
function blurJS(dados, largura, altura) { /* ... */ }

// Rust + Wasm: ~12ms
blur_gaussiano(dados, largura, altura);

A regra prática: use Wasm para operações que processam muitos dados ou executam muitas iterações. Para operações pontuais, o overhead de chamada não vale.


Testes para código Wasm

wasm-bindgen-test permite testar código Wasm no navegador ou no Node.js via headless:

use wasm_bindgen_test::*;

wasm_bindgen_test_configure!(run_in_browser);

#[wasm_bindgen_test]
fn teste_para_cinza() {
    // Pixel vermelho puro RGBA
    let mut dados = vec![255u8, 0, 0, 255];
    para_cinza(&mut dados);

    // Vermelho puro vira cinza com luminância ~76
    assert!((dados[0] as i32 - 76).abs() < 5);
    assert_eq!(dados[0], dados[1]);
    assert_eq!(dados[1], dados[2]);
    assert_eq!(dados[3], 255); // alpha não muda
}

#[wasm_bindgen_test]
fn teste_inverter() {
    let mut dados = vec![100u8, 150, 200, 255];
    inverter(&mut dados);
    assert_eq!(dados[0], 155);
    assert_eq!(dados[1], 105);
    assert_eq!(dados[2], 55);
    assert_eq!(dados[3], 255);
}

#[wasm_bindgen_test]
fn teste_brilho_positivo() {
    let mut dados = vec![200u8, 200, 200, 255];
    ajustar_brilho(&mut dados, 100);
    // 200 + 100 = 300, mas clamp para 255
    assert_eq!(dados[0], 255);
}

Execute:

wasm-pack test --headless --firefox
# ou
wasm-pack test --headless --chrome

O ecossistema Wasm além do navegador

Rust+Wasm não é só para navegadores. O formato Wasm também roda em:

WASI (WebAssembly System Interface) — Wasm fora do navegador, com acesso controlado ao sistema de arquivos, rede e outras APIs do OS. Ideal para plugins sandboxed e computação edge.

Cloudflare Workers e Fastly Compute — execute Rust compilado para Wasm no edge, com latência mínima e sem cold starts pesados.

Wasmtime e Wasmer — runtimes standalone que executam Wasm em servidores. Use Rust para escrever plugins portáveis que rodam em qualquer plataforma.

# Compilar para WASI
rustup target add wasm32-wasi
cargo build --target wasm32-wasi --release

# Executar com wasmtime
wasmtime target/wasm32-wasi/release/meu_programa.wasm

Fontes e leituras recomendadas

  • "Rust and WebAssembly" — The Rust Wasm Book — guia oficial completo — https://rustwasm.github.io/docs/book/
  • wasm-bindgen Guide — https://rustwasm.github.io/docs/wasm-bindgen/
  • wasm-pack Documentation — https://rustwasm.github.io/docs/wasm-pack/
  • "Hello wasm-pack!" — tutorial oficial — https://hacks.mozilla.org/2018/04/hello-wasm-pack/
  • WebAssembly.org — especificação e recursos — https://webassembly.org
  • Leptos — framework frontend em Rust que compila para Wasm — https://leptos.dev
  • Yew — framework React-like em Rust — https://yew.rs
  • WASI — WebAssembly System Interface — https://wasi.dev
Comentários

Mais em Rust

Macros — Escrevendo Código que Escreve Código
Macros — Escrevendo Código que Escreve Código

Desde o primeiro artigo usamos macros sem entender completamente o que são: p...

Testes — Escrevendo Código que Prova que Seu Código Funciona
Testes — Escrevendo Código que Prova que Seu Código Funciona

&nbsp; Rust tem suporte nativo a testes &mdash; sem frameworks externos, sem...

Módulos e Organização de Código — Estruturando Projetos Reais
Módulos e Organização de Código — Estruturando Projetos Reais

Até agora todos os nossos exemplos viveram em um único arquivo main.rs. Isso...