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 Wasmcalculadora_wasm.js— wrapper JavaScript gerado automaticamentecalculadora_wasm.d.ts— definições TypeScriptpackage.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-bindgenGuide — https://rustwasm.github.io/docs/wasm-bindgen/wasm-packDocumentation — 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