WebAssembly (Wasm) é um formato binário que permite executar código de alta performance diretamente no navegador (e fora dele). A crate wasm-bindgen é a ponte que conecta Rust ao mundo JavaScript, permitindo que funções Rust sejam chamadas de JS e vice-versa, com conversão automática de tipos.
Rust é uma das linguagens mais bem posicionadas para WebAssembly: sem garbage collector, sem runtime pesado, com controle fino de memória e performance próxima ao código nativo. Com wasm-bindgen, web-sys e wasm-pack, você tem um ecossistema completo para criar desde bibliotecas de processamento de dados até aplicações web interativas.
Instalação
Pré-requisitos
Instale as ferramentas necessárias:
# Target de compilação WASM
rustup target add wasm32-unknown-unknown
# wasm-pack: ferramenta de build e publicação
cargo install wasm-pack
# Opcional: servidor de desenvolvimento
cargo install miniserve
Criando um Projeto WASM
# Usando wasm-pack template
cargo init meu-projeto-wasm --lib
cd meu-projeto-wasm
Configure o Cargo.toml:
[package]
name = "meu-projeto-wasm"
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",
"Event",
"MouseEvent",
"KeyboardEvent",
"HtmlInputElement",
"CssStyleDeclaration",
"DomTokenList",
] }
[profile.release]
opt-level = "s" # Otimizar para tamanho
lto = true # Link-Time Optimization
Uso Básico
Exportando Funções para JavaScript
// src/lib.rs
use wasm_bindgen::prelude::*;
// Exportar uma função simples
#[wasm_bindgen]
pub fn somar(a: i32, b: i32) -> i32 {
a + b
}
// Exportar com nome diferente em JS
#[wasm_bindgen(js_name = cumprimentar)]
pub fn saudar(nome: &str) -> String {
format!("Olá, {}! Bem-vindo ao Rust + WASM!", nome)
}
// Função que recebe e retorna tipos complexos
#[wasm_bindgen]
pub fn fibonacci(n: u32) -> Vec<u32> {
let mut seq = vec![0, 1];
for i in 2..n as usize {
let proximo = seq[i - 1] + seq[i - 2];
seq.push(proximo);
}
seq
}
// Função que pode falhar
#[wasm_bindgen]
pub fn dividir(a: f64, b: f64) -> Result<f64, JsValue> {
if b == 0.0 {
Err(JsValue::from_str("Divisão por zero!"))
} else {
Ok(a / b)
}
}
Compilando e Usando
# Compilar com wasm-pack
wasm-pack build --target web
# Isso gera a pasta pkg/ com:
# - meu_projeto_wasm_bg.wasm (binário WASM)
# - meu_projeto_wasm.js (glue code JS)
# - meu_projeto_wasm.d.ts (tipos TypeScript)
# - package.json
Usando no HTML:
<!DOCTYPE html>
<html>
<head>
<title>Rust + WASM</title>
</head>
<body>
<h1>Rust WebAssembly</h1>
<div id="resultado"></div>
<script type="module">
import init, { somar, cumprimentar, fibonacci, dividir }
from './pkg/meu_projeto_wasm.js';
async function main() {
await init();
console.log("2 + 3 =", somar(2, 3));
console.log(cumprimentar("Maria"));
console.log("Fibonacci:", fibonacci(10));
try {
console.log("10 / 3 =", dividir(10, 3));
console.log("10 / 0 =", dividir(10, 0));
} catch (e) {
console.error("Erro:", e);
}
document.getElementById("resultado").textContent =
cumprimentar("Mundo");
}
main();
</script>
</body>
</html>
Importando Funções JavaScript
use wasm_bindgen::prelude::*;
// Importar funções globais do JS
#[wasm_bindgen]
extern "C" {
// window.alert()
fn alert(s: &str);
// console.log()
#[wasm_bindgen(js_namespace = console)]
fn log(s: &str);
#[wasm_bindgen(js_namespace = console, js_name = log)]
fn log_u32(a: u32);
#[wasm_bindgen(js_namespace = console, js_name = log)]
fn log_many(a: &str, b: &str);
// performance.now()
#[wasm_bindgen(js_namespace = performance)]
fn now() -> f64;
}
// Macro auxiliar para console.log
macro_rules! console_log {
($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}
#[wasm_bindgen]
pub fn demonstrar_imports() {
console_log!("Olá do Rust via console.log!");
console_log!("Performance.now(): {:.2}ms", now());
log_many("Múltiplos", "argumentos");
}
Structs Exportadas
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct Contador {
valor: i32,
historico: Vec<i32>,
}
#[wasm_bindgen]
impl Contador {
#[wasm_bindgen(constructor)]
pub fn novo(valor_inicial: i32) -> Contador {
Contador {
valor: valor_inicial,
historico: vec![valor_inicial],
}
}
pub fn incrementar(&mut self) {
self.valor += 1;
self.historico.push(self.valor);
}
pub fn decrementar(&mut self) {
self.valor -= 1;
self.historico.push(self.valor);
}
pub fn valor(&self) -> i32 {
self.valor
}
pub fn resetar(&mut self, valor: i32) {
self.valor = valor;
self.historico.clear();
self.historico.push(valor);
}
pub fn historico_json(&self) -> String {
format!("{:?}", self.historico)
}
}
Uso em JavaScript:
import { Contador } from './pkg/meu_projeto_wasm.js';
const contador = new Contador(0);
contador.incrementar();
contador.incrementar();
contador.incrementar();
contador.decrementar();
console.log("Valor:", contador.valor()); // 2
console.log("Histórico:", contador.historico_json()); // [0, 1, 2, 3, 2]
contador.free(); // Liberar memória WASM
Recursos Avançados
web-sys: APIs do Navegador
A crate web-sys fornece bindings para todas as Web APIs:
use wasm_bindgen::prelude::*;
use web_sys::{console, Document, Element, HtmlElement, Window};
fn window() -> Window {
web_sys::window().expect("Sem window global")
}
fn document() -> Document {
window().document().expect("Sem document")
}
#[wasm_bindgen]
pub fn manipular_dom() -> Result<(), JsValue> {
let document = document();
// Criar elemento
let div = document.create_element("div")?;
div.set_id("meu-div");
div.set_class_name("container ativo");
div.set_inner_html("<h2>Criado pelo Rust!</h2><p>Manipulação de DOM via wasm-bindgen.</p>");
// Estilizar
let estilo = div
.dyn_ref::<HtmlElement>()
.unwrap()
.style();
estilo.set_property("background-color", "#f0f0f0")?;
estilo.set_property("padding", "20px")?;
estilo.set_property("border-radius", "8px")?;
estilo.set_property("margin", "10px")?;
// Adicionar ao body
let body = document.body().unwrap();
body.append_child(&div)?;
// Buscar elementos existentes
if let Some(titulo) = document.get_element_by_id("titulo") {
titulo.set_text_content(Some("Título atualizado pelo Rust!"));
}
// Query selector
let paragrafos = document.query_selector_all("p")?;
console::log_1(&format!("Encontrados {} parágrafos", paragrafos.length()).into());
Ok(())
}
Event Listeners com Closures
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{Document, Event, HtmlInputElement, MouseEvent};
#[wasm_bindgen]
pub fn configurar_eventos() -> Result<(), JsValue> {
let document = web_sys::window().unwrap().document().unwrap();
// Evento de clique
let botao = document.get_element_by_id("meu-botao").unwrap();
let callback = Closure::wrap(Box::new(move |evento: MouseEvent| {
let x = evento.client_x();
let y = evento.client_y();
web_sys::console::log_1(
&format!("Clique em ({}, {})", x, y).into(),
);
}) as Box<dyn FnMut(MouseEvent)>);
botao.add_event_listener_with_callback(
"click",
callback.as_ref().unchecked_ref(),
)?;
callback.forget(); // Manter o closure vivo
// Evento de input
let input = document.get_element_by_id("meu-input").unwrap();
let output = document.get_element_by_id("output").unwrap();
let callback_input = Closure::wrap(Box::new(move |evento: Event| {
let target = evento.target().unwrap();
let input = target.dyn_ref::<HtmlInputElement>().unwrap();
let valor = input.value();
let maiusculo = valor.to_uppercase();
if let Some(el) = web_sys::window()
.unwrap()
.document()
.unwrap()
.get_element_by_id("output")
{
el.set_text_content(Some(&maiusculo));
}
}) as Box<dyn FnMut(Event)>);
input.add_event_listener_with_callback(
"input",
callback_input.as_ref().unchecked_ref(),
)?;
callback_input.forget();
Ok(())
}
js-sys: Tipos JavaScript Nativos
use wasm_bindgen::prelude::*;
use js_sys::{Array, Date, JSON, Map, Object, Promise, Reflect};
#[wasm_bindgen]
pub fn demonstrar_js_sys() -> Result<JsValue, JsValue> {
// Date
let agora = Date::new_0();
web_sys::console::log_1(&format!(
"Data JS: {}/{}/{}",
agora.get_date(),
agora.get_month() + 1,
agora.get_full_year()
).into());
// Array
let arr = Array::new();
arr.push(&JsValue::from("Rust"));
arr.push(&JsValue::from("WebAssembly"));
arr.push(&JsValue::from("JavaScript"));
web_sys::console::log_1(&format!("Array length: {}", arr.length()).into());
// Map
let mapa = Map::new();
mapa.set(&JsValue::from("linguagem"), &JsValue::from("Rust"));
mapa.set(&JsValue::from("versao"), &JsValue::from("1.75"));
// Object
let obj = Object::new();
Reflect::set(&obj, &"nome".into(), &"wasm-bindgen".into())?;
Reflect::set(&obj, &"versao".into(), &"0.2".into())?;
// JSON
let json_str = JSON::stringify(&obj)?;
web_sys::console::log_1(&json_str);
Ok(obj.into())
}
Processamento de Dados Pesado
Um dos melhores casos de uso para WASM: processamento intensivo de dados:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn processar_imagem(dados: &[u8], largura: u32, altura: u32) -> Vec<u8> {
let mut resultado = dados.to_vec();
// Converter para escala de cinza
for pixel in resultado.chunks_exact_mut(4) {
let r = pixel[0] as f32;
let g = pixel[1] as f32;
let b = pixel[2] as f32;
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) permanece inalterado
}
resultado
}
#[wasm_bindgen]
pub fn aplicar_blur(dados: &[u8], largura: u32, altura: u32, raio: u32) -> Vec<u8> {
let mut resultado = dados.to_vec();
let w = largura as usize;
let h = altura as usize;
let r = raio as usize;
for y in r..h - r {
for x in r..w - r {
let mut soma_r = 0u32;
let mut soma_g = 0u32;
let mut soma_b = 0u32;
let mut contagem = 0u32;
for dy in 0..=2 * r {
for dx in 0..=2 * r {
let ny = y + dy - r;
let nx = x + dx - r;
let idx = (ny * w + nx) * 4;
soma_r += dados[idx] as u32;
soma_g += dados[idx + 1] as u32;
soma_b += dados[idx + 2] as u32;
contagem += 1;
}
}
let idx = (y * w + x) * 4;
resultado[idx] = (soma_r / contagem) as u8;
resultado[idx + 1] = (soma_g / contagem) as u8;
resultado[idx + 2] = (soma_b / contagem) as u8;
}
}
resultado
}
// Sorting eficiente em WASM
#[wasm_bindgen]
pub fn ordenar_numeros(mut numeros: Vec<f64>) -> Vec<f64> {
numeros.sort_by(|a, b| a.partial_cmp(b).unwrap());
numeros
}
// Busca em texto (muito mais rápido que JS para textos grandes)
#[wasm_bindgen]
pub fn contar_ocorrencias(texto: &str, padrao: &str) -> u32 {
texto.matches(padrao).count() as u32
}
Workflow com wasm-pack
# Build para web (ES modules)
wasm-pack build --target web
# Build para bundlers (webpack, vite, etc.)
wasm-pack build --target bundler
# Build para Node.js
wasm-pack build --target nodejs
# Build com otimizações
wasm-pack build --release --target web
# Testar no navegador
wasm-pack test --chrome --headless
# Publicar no npm
wasm-pack publish
Estrutura do pkg/ gerado:
pkg/
├── meu_projeto_wasm_bg.wasm # Binário WASM
├── meu_projeto_wasm_bg.wasm.d.ts # Tipos do WASM
├── meu_projeto_wasm.js # Glue code JS
├── meu_projeto_wasm.d.ts # Tipos TypeScript
├── package.json # Para npm
└── README.md
Boas Práticas
1. Minimize as Chamadas entre JS e WASM
use wasm_bindgen::prelude::*;
// RUIM: muitas chamadas individuais JS -> WASM
#[wasm_bindgen]
pub fn somar_um(n: f64) -> f64 {
n + 1.0
}
// JS: for (let i = 0; i < 1000; i++) { arr[i] = somar_um(arr[i]); }
// BOM: processar em lote
#[wasm_bindgen]
pub fn somar_um_lote(dados: &[f64]) -> Vec<f64> {
dados.iter().map(|n| n + 1.0).collect()
}
// JS: arr = somar_um_lote(arr);
2. Gerencie Memória Corretamente
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub struct RecursoPesado {
dados: Vec<u8>,
}
#[wasm_bindgen]
impl RecursoPesado {
#[wasm_bindgen(constructor)]
pub fn novo(tamanho: usize) -> RecursoPesado {
RecursoPesado {
dados: vec![0; tamanho],
}
}
// Método free() é gerado automaticamente pelo wasm-bindgen
// JS deve chamar recurso.free() quando não precisar mais
}
// Uso correto em JS:
const recurso = new RecursoPesado(1024 * 1024);
// ... usar recurso ...
recurso.free(); // IMPORTANTE: liberar memória WASM
3. Use #[wasm_bindgen(start)] para Inicialização
use wasm_bindgen::prelude::*;
#[wasm_bindgen(start)]
pub fn inicializar() {
// Configurar panic hook para mensagens de erro úteis
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
web_sys::console::log_1(&"Módulo WASM inicializado!".into());
}
4. Otimize o Tamanho do WASM
# Cargo.toml
[profile.release]
opt-level = "s" # Otimizar para tamanho (ou "z" para menor possível)
lto = true # Link-Time Optimization
codegen-units = 1 # Melhor otimização
strip = true # Remover símbolos de debug
[dependencies]
console_error_panic_hook = { version = "0.1", optional = true }
wee_alloc = { version = "0.4", optional = true }
[features]
default = ["console_error_panic_hook"]
# Usar wasm-opt para otimização adicional (instalado com wasm-pack)
wasm-pack build --release
5. Tratamento de Erros Informativo
use wasm_bindgen::prelude::*;
// Configurar panic hook para erros legíveis no console do navegador
pub fn configurar_panic_hook() {
#[cfg(feature = "console_error_panic_hook")]
console_error_panic_hook::set_once();
}
#[wasm_bindgen]
pub fn operacao_que_pode_falhar(dados: &str) -> Result<String, JsValue> {
configurar_panic_hook();
serde_json::from_str::<serde_json::Value>(dados)
.map(|v| v.to_string())
.map_err(|e| JsValue::from_str(&format!("Erro de parsing: {}", e)))
}
Exemplos Práticos
Exemplo Completo: Componente Web Interativo
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use web_sys::{
CanvasRenderingContext2d, Document, HtmlCanvasElement,
HtmlElement, MouseEvent, Window,
};
use std::cell::RefCell;
use std::rc::Rc;
fn window() -> Window {
web_sys::window().unwrap()
}
fn document() -> Document {
window().document().unwrap()
}
#[wasm_bindgen]
pub struct App {
canvas: HtmlCanvasElement,
ctx: CanvasRenderingContext2d,
}
#[derive(Clone)]
struct Ponto {
x: f64,
y: f64,
cor: String,
raio: f64,
}
#[wasm_bindgen]
impl App {
#[wasm_bindgen(constructor)]
pub fn novo(canvas_id: &str) -> Result<App, JsValue> {
let document = document();
let canvas = document
.get_element_by_id(canvas_id)
.ok_or_else(|| JsValue::from_str("Canvas não encontrado"))?
.dyn_into::<HtmlCanvasElement>()?;
let ctx = canvas
.get_context("2d")?
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()?;
Ok(App { canvas, ctx })
}
pub fn iniciar(&self) -> Result<(), JsValue> {
let largura = self.canvas.width() as f64;
let altura = self.canvas.height() as f64;
// Fundo
self.ctx.set_fill_style_str("#1a1a2e");
self.ctx.fill_rect(0.0, 0.0, largura, altura);
// Título
self.ctx.set_fill_style_str("#e94560");
self.ctx.set_font("24px Arial");
self.ctx.set_text_align("center");
self.ctx
.fill_text("Clique para desenhar!", largura / 2.0, 40.0)?;
// Configurar evento de clique
let canvas = self.canvas.clone();
let ctx = self
.canvas
.get_context("2d")
.unwrap()
.unwrap()
.dyn_into::<CanvasRenderingContext2d>()
.unwrap();
let pontos = Rc::new(RefCell::new(Vec::<Ponto>::new()));
let pontos_clone = pontos.clone();
let callback = Closure::wrap(Box::new(move |evento: MouseEvent| {
let rect = canvas.get_bounding_client_rect();
let x = evento.client_x() as f64 - rect.left();
let y = evento.client_y() as f64 - rect.top();
let cores = ["#e94560", "#0f3460", "#16213e", "#533483", "#e94560"];
let idx = pontos_clone.borrow().len() % cores.len();
let ponto = Ponto {
x,
y,
cor: cores[idx].to_string(),
raio: 5.0 + (pontos_clone.borrow().len() as f64 % 20.0),
};
// Desenhar círculo
ctx.begin_path();
ctx.arc(ponto.x, ponto.y, ponto.raio, 0.0, std::f64::consts::PI * 2.0)
.unwrap();
ctx.set_fill_style_str(&ponto.cor);
ctx.fill();
// Conectar ao ponto anterior
let pontos_ref = pontos_clone.borrow();
if let Some(anterior) = pontos_ref.last() {
ctx.begin_path();
ctx.move_to(anterior.x, anterior.y);
ctx.line_to(ponto.x, ponto.y);
ctx.set_stroke_style_str(&ponto.cor);
ctx.set_line_width(2.0);
ctx.stroke();
}
drop(pontos_ref);
pontos_clone.borrow_mut().push(ponto);
}) as Box<dyn FnMut(MouseEvent)>);
self.canvas.add_event_listener_with_callback(
"click",
callback.as_ref().unchecked_ref(),
)?;
callback.forget();
Ok(())
}
pub fn limpar(&self) {
let largura = self.canvas.width() as f64;
let altura = self.canvas.height() as f64;
self.ctx.set_fill_style_str("#1a1a2e");
self.ctx.fill_rect(0.0, 0.0, largura, altura);
}
pub fn desenhar_grafico(&self, dados: &[f64]) -> Result<(), JsValue> {
let largura = self.canvas.width() as f64;
let altura = self.canvas.height() as f64;
let margem = 50.0;
// Limpar
self.ctx.set_fill_style_str("#1a1a2e");
self.ctx.fill_rect(0.0, 0.0, largura, altura);
if dados.is_empty() {
return Ok(());
}
let max_val = dados.iter().cloned().fold(f64::NEG_INFINITY, f64::max);
let min_val = dados.iter().cloned().fold(f64::INFINITY, f64::min);
let range = if max_val == min_val { 1.0 } else { max_val - min_val };
let area_largura = largura - 2.0 * margem;
let area_altura = altura - 2.0 * margem;
let passo = area_largura / (dados.len() - 1).max(1) as f64;
// Eixos
self.ctx.set_stroke_style_str("#555");
self.ctx.set_line_width(1.0);
self.ctx.begin_path();
self.ctx.move_to(margem, margem);
self.ctx.line_to(margem, altura - margem);
self.ctx.line_to(largura - margem, altura - margem);
self.ctx.stroke();
// Linha do gráfico
self.ctx.set_stroke_style_str("#e94560");
self.ctx.set_line_width(2.0);
self.ctx.begin_path();
for (i, &valor) in dados.iter().enumerate() {
let x = margem + i as f64 * passo;
let y = altura - margem - ((valor - min_val) / range * area_altura);
if i == 0 {
self.ctx.move_to(x, y);
} else {
self.ctx.line_to(x, y);
}
}
self.ctx.stroke();
// Pontos
self.ctx.set_fill_style_str("#e94560");
for (i, &valor) in dados.iter().enumerate() {
let x = margem + i as f64 * passo;
let y = altura - margem - ((valor - min_val) / range * area_altura);
self.ctx.begin_path();
self.ctx.arc(x, y, 4.0, 0.0, std::f64::consts::PI * 2.0)?;
self.ctx.fill();
}
// Labels
self.ctx.set_fill_style_str("#aaa");
self.ctx.set_font("12px monospace");
self.ctx.set_text_align("right");
self.ctx.fill_text(&format!("{:.1}", max_val), margem - 5.0, margem + 5.0)?;
self.ctx.fill_text(
&format!("{:.1}", min_val),
margem - 5.0,
altura - margem + 5.0,
)?;
Ok(())
}
}
HTML para o exemplo:
<!DOCTYPE html>
<html>
<head>
<title>Rust WASM Canvas</title>
<style>
body {
background: #0f0f23;
color: white;
font-family: Arial, sans-serif;
display: flex;
flex-direction: column;
align-items: center;
padding: 20px;
}
canvas {
border: 2px solid #333;
border-radius: 8px;
cursor: crosshair;
}
button {
margin: 10px;
padding: 10px 20px;
background: #e94560;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
}
button:hover { background: #c73e55; }
</style>
</head>
<body>
<h1>Rust + WebAssembly Canvas</h1>
<canvas id="canvas" width="800" height="500"></canvas>
<div>
<button id="btn-limpar">Limpar</button>
<button id="btn-grafico">Gráfico Demo</button>
</div>
<script type="module">
import init, { App } from './pkg/meu_projeto_wasm.js';
async function main() {
await init();
const app = new App("canvas");
app.iniciar();
document.getElementById("btn-limpar").onclick = () => {
app.limpar();
app.iniciar();
};
document.getElementById("btn-grafico").onclick = () => {
const dados = Array.from({length: 20}, () => Math.random() * 100);
app.desenhar_grafico(new Float64Array(dados));
};
}
main();
</script>
</body>
</html>
Comparação com Alternativas
| Tecnologia | Caso de uso | Destaques |
|---|---|---|
wasm-bindgen | Interop Rust-JS | Mais maduro, tipagem rica |
stdweb | Interop Rust-JS | Descontinuado, use wasm-bindgen |
Yew | Framework SPA | Similar ao React, componentes |
Leptos | Framework SPA | Signals, SSR, muito performático |
Dioxus | Framework multiplataforma | Web, desktop, mobile |
Trunk | Build tool | Bundling para projetos WASM |
Conclusão
A combinação de wasm-bindgen, web-sys e wasm-pack torna Rust uma opção de primeira classe para WebAssembly. Desde processamento de imagens e criptografia até jogos e visualizações de dados, o WASM com Rust entrega performance nativa no navegador com segurança de memória.
O workflow moderno com wasm-pack simplifica build, testes e publicação, gerando automaticamente tipos TypeScript e pacotes npm. A interoperabilidade bidirecional entre Rust e JavaScript, com conversão automática de tipos, torna o desenvolvimento surpreendentemente ergonômico.
Próximos passos: