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-packsimplificam o workflow - Interop com JS:
wasm-bindgenfacilita 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.