Rust e WebAssembly
O WebAssembly (muitas vezes abreviado como Wasm) permite rodar código escrito em linguagens como Rust dentro do navegador, ao lado do JavaScript. Pense nele como um segundo motor no navegador: o JavaScript continua controlando a página, mas, quando surge um trabalho pesado (filtros de imagem, física, ler arquivos grandes), você pode entregá-lo a um código Rust compilado e rápido.
Nesta página você vai escrever uma pequena biblioteca em Rust, compilá-la para Wasm e chamá-la a partir de uma página web. Nada disso roda no Rust Playground (é preciso um navegador e ferramentas extras), então a maioria dos exemplos são blocos de código comuns para você rodar na sua máquina. As partes em Rust puro, porém, você pode experimentar na hora.
O que é WebAssembly, e por que o Rust combina com ele
Seção intitulada “O que é WebAssembly, e por que o Rust combina com ele”WebAssembly é um formato binário compacto para programas. Todos os principais navegadores conseguem rodá-lo, quase na velocidade nativa, dentro da mesma sandbox de segurança do JavaScript. E não é só para navegadores: servidores e plataformas de edge também rodam Wasm.
O Rust é uma das linguagens mais populares para Wasm, por alguns motivos:
- Sem garbage collector nem runtime grande. O Rust não precisa levar uma maquinaria extra junto com o seu código, então os arquivos
.wasmpodem ser pequenos. - Ótimas ferramentas. O
wasm-bindgenescreve para você o código de ligação entre Rust e JavaScript, e owasm-packcompila tudo com um só comando. - Segurança e velocidade. Você tem a mesma segurança de memória e o mesmo desempenho que teria em um computador.
Instale as ferramentas
Seção intitulada “Instale as ferramentas”Primeiro, diga ao Rust que ele pode compilar para o target (alvo) Wasm. Um target é “o tipo de máquina em que o código vai rodar”; wasm32-unknown-unknown significa “Wasm de 32 bits, sem nenhum sistema operacional em particular”:
rustup target add wasm32-unknown-unknownEm seguida, instale o wasm-pack. Ele compila o seu crate (pacote), roda o wasm-bindgen e coloca tudo o que você precisa em uma pasta. Use o instalador oficial ou compile com o Cargo:
# Instalador para Linux / macOS, do site do wasm-packcurl https://wasm-bindgen.github.io/wasm-pack/installer/init.sh -sSf | sh
# ou, em qualquer sistema com Rust instaladocargo install wasm-pack --lockedEscreva uma biblioteca Rust para Wasm
Seção intitulada “Escreva uma biblioteca Rust para Wasm”Um módulo Wasm é uma biblioteca, não um programa com main. Crie uma:
cargo new --lib hello-wasmcd hello-wasmDepois edite o Cargo.toml. A linha crate-type diz ao Cargo para gerar uma biblioteca adequada para .wasm (cdylib), e o wasm-bindgen é o crate que conecta Rust e JavaScript:
[package]name = "hello-wasm"version = "0.1.0"edition = "2021"
[lib]crate-type = ["cdylib", "rlib"]
[dependencies]wasm-bindgen = "0.2"Agora escreva duas funções em src/lib.rs. O atributo #[wasm_bindgen] marca cada uma como “pode ser chamada pelo JavaScript”:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]pub fn add(a: i32, b: i32) -> i32 { a + b}
#[wasm_bindgen]pub fn greet(name: &str) -> String { format!("Olá, {name}! Esta string foi criada em Rust.")}É todo o Rust de que você precisa. Repare que são funções comuns: o wasm-bindgen cuida de transformar uma string do JavaScript em um &str, e a String devolvida de volta em uma string do JavaScript.
Teste a lógica nativamente primeiro
Seção intitulada “Teste a lógica nativamente primeiro”Um hábito que economiza muito tempo: deixe o trabalho de verdade em funções Rust comuns e teste-as no seu computador com cargo test ou um main rápido. Depurar no navegador é mais lento do que depurar no terminal. A mesma lógica, sem o atributo, roda em qualquer lugar:
fn add(a: i32, b: i32) -> i32 { a + b}
fn greet(name: &str) -> String { format!("Olá, {name}! Esta string foi criada em Rust.")}
fn main() { println!("{}", greet("Ferris")); println!("2 + 3 = {}", add(2, 3));}Saída:
Olá, Ferris! Esta string foi criada em Rust.2 + 3 = 5Quando a lógica estiver certa, o invólucro #[wasm_bindgen] é só uma porta fina para o navegador. (Testes marcados com #[test] também rodam nativamente com cargo test; veja Testes.)
Compile com o wasm-pack
Seção intitulada “Compile com o wasm-pack”Na pasta do crate, rode:
wasm-pack build --target web--target web significa “gere arquivos que um navegador consiga carregar direto com uma tag <script type="module">”, sem precisar de um bundler como o webpack. Quando rodamos, a nova pasta pkg/ continha:
pkg/├── hello_wasm_bg.wasm o código Rust compilado├── hello_wasm.js código JavaScript de ligação que carrega o módulo e envolve cada função├── hello_wasm.d.ts tipos TypeScript para as suas funções├── hello_wasm_bg.wasm.d.ts└── package.jsonO hífen de hello-wasm virou um sublinhado nos nomes dos arquivos. Por padrão, wasm-pack build faz um build de release (otimizado); acrescente --dev para um build mais rápido e sem otimização enquanto você experimenta.
Chame pelo JavaScript
Seção intitulada “Chame pelo JavaScript”Crie um index.html ao lado do Cargo.toml (para que a pasta pkg/ fique junto dele):
<!doctype html><html lang="pt-BR"><head> <meta charset="utf-8"> <title>Olá do Rust</title></head><body> <p id="message">Carregando…</p>
<script type="module"> import init, { add, greet } from "./pkg/hello_wasm.js";
await init(); // baixa e inicia o arquivo .wasm
const text = greet("Ferris"); const sum = add(2, 3); document.getElementById("message").textContent = `${text} 2 + 3 = ${sum}`; </script></body></html>O export padrão, init(), baixa o arquivo .wasm e o inicia. Depois disso, add e greet são funções JavaScript normais.
Os navegadores não carregam módulos de um endereço file://, então você precisa de um pequeno servidor web local. Se você tem Python instalado, este é o jeito mais rápido:
python3 -m http.server 8080Abra http://localhost:8080 no navegador. Quando fizemos isso, a página mostrou:
Olá, Ferris! Esta string foi criada em Rust. 2 + 3 = 5Essa frase foi montada pelo Rust, rodando dentro do navegador. 🦀
O que atravessa a fronteira de graça (e o que não)
Seção intitulada “O que atravessa a fronteira de graça (e o que não)”Rust e JavaScript não compartilham a memória do mesmo jeito. O módulo Wasm tem o seu próprio bloco de memória, e os valores precisam viajar entre os dois lados. Algumas viagens são de graça; outras custam uma cópia:
| Tipo em Rust | Em JavaScript | Custo |
|---|---|---|
i32, u32, f64, bool |
number, boolean |
Barato: passado diretamente |
&str, String |
string |
Uma cópia, mais a conversão de UTF-16 para UTF-8 (ou o contrário) |
&[u8], Vec<u8> |
Uint8Array |
Uma cópia para dentro (ou para fora) da memória do Wasm |
&mut [u8] |
Uint8Array |
Copiado para dentro e, depois da chamada, copiado de volta |
Então a regra de ouro é: faça poucas chamadas, e que cada uma faça bastante trabalho. Chamar o Rust uma vez para processar uma imagem inteira é rápido. Chamar o Rust um milhão de vezes, uma por pixel, gasta a maior parte do tempo atravessando a fronteira.
Veja como fica. Esta função clareia um buffer inteiro de valores de pixels em uma única chamada:
use wasm_bindgen::prelude::*;
/// Deixa cada pixel mais claro. O JS passa um Uint8Array.#[wasm_bindgen]pub fn brighten(pixels: &mut [u8], amount: u8) { for p in pixels.iter_mut() { *p = p.saturating_add(amount); }}No JavaScript, depois de await init(), você chamaria brighten(pixels, 20) com um Uint8Array, e o array é atualizado no próprio lugar. O laço em si é Rust puro, então dá para testar nativamente:
fn brighten(pixels: &mut [u8], amount: u8) { for p in pixels.iter_mut() { // saturating_add para em 255 em vez de dar overflow *p = p.saturating_add(amount); }}
fn main() { let mut pixels = [10, 100, 250]; brighten(&mut pixels, 20); println!("{pixels:?}");}Saída:
[30, 120, 255]Repare que 250 + 20 virou 255, e não um erro nem uma volta para o zero: saturating_add para no valor máximo. É exatamente o que você quer para cores.
Mantenha o arquivo .wasm pequeno
Seção intitulada “Mantenha o arquivo .wasm pequeno”Cada byte do seu arquivo .wasm precisa ser baixado antes que a página possa usá-lo. Algumas dicas:
- Sempre publique o build de release. No nosso exemplo minúsculo,
wasm-pack build --devgerou um arquivo.wasmde 58.591 bytes; o build de release padrão ficou com 21.530 bytes. (Isso na nossa máquina, com owasm-optdesligado; os seus números vão variar.) - Deixe o wasm-pack rodar o
wasm-opt. No modo release, o wasm-pack roda por padrão o otimizadorwasm-opt, do Binaryen, sobre o resultado, o que normalmente o encolhe ainda mais. - Ative a compressão no seu servidor. O nosso arquivo de 21.530 bytes ficou com 8.464 bytes depois do gzip.
- Experimente configurações focadas em tamanho e depois meça:
[profile.release]opt-level = "z" # otimiza para tamanho ("s" é uma versão mais leve)lto = truecodegen-units = 1Seja honesto consigo mesmo e meça. No nosso exemplo de duas funções, essas configurações não ajudaram: o arquivo saiu com 22.817 bytes, um pouco maior. Elas costumam valer a pena em crates maiores. Coisas como format! e mensagens de panic também puxam código, então um crate que as evita pode ficar bem menor.
Onde o Wasm é usado no mundo real
Seção intitulada “Onde o Wasm é usado no mundo real”Alguns exemplos, cada um com o relato da própria empresa (em inglês):
- O Figma compila o seu editor de design, escrito em C++, para WebAssembly e contou que isso reduziu o tempo de carregamento em 3 vezes. (Não é Rust, mas mostra o que o Wasm consegue fazer em um app grande.)
- O Cloudflare Workers tem suporte a Rust por meio do crate
workers-rs: o seu código Rust é compilado para Wasm e roda nos servidores da Cloudflare pelo mundo todo. - As Shopify Functions são módulos WebAssembly que personalizam lojas, e a Shopify recomenda o Rust como a linguagem de melhor desempenho para elas.
- A extensão de navegador do 1Password passou o preenchimento de páginas para WebAssembly e contou que ele ficou pelo menos duas vezes mais rápido.
Erros comuns
Seção intitulada “Erros comuns”Esquecer crate-type = ["cdylib"]. Sem isso, o wasm-pack para na hora. Felizmente, o erro diz exatamente o que acrescentar:
Error: crate-type must be cdylib to compile to WebAssembly. Add the following to your Cargo.toml file:
[lib]crate-type = ["cdylib", "rlib"]A mensagem diz que o crate-type precisa ser cdylib para compilar para WebAssembly, e mostra as linhas para colocar no seu Cargo.toml.
Abrir o index.html com um clique duplo. A página fica parada em “Carregando…”, e o console do navegador (no Chrome) diz:
Access to script at 'file:///…/pkg/hello_wasm.js' from origin 'null' has been blocked by CORS policy: Cross origin requests are only supported for protocol schemes: chrome, chrome-extension, chrome-untrusted, data, http, https, isolated-app.Ou seja, a política de CORS bloqueou o script, porque requisições desse tipo só funcionam com protocolos como http e https, e não com file. A solução: sirva a pasta com um servidor web local, como python3 -m http.server, e abra o endereço http://localhost.
Chamar uma função antes de await init(). O módulo Wasm ainda não foi carregado, então o código de ligação não tem o que chamar. Quando removemos a linha do init(), o Chrome mostrou Cannot read properties of undefined (reading '__wbindgen_free') (“não é possível ler propriedades de undefined”). Sempre faça await init() primeiro.
Próximos passos: apps web completos em Rust
Seção intitulada “Próximos passos: apps web completos em Rust”Chamar algumas funções Rust pelo JavaScript é um ótimo começo. Se você quer construir o app web inteiro em Rust, dê uma olhada nestes frameworks. Os dois compilam para Wasm e deixam você escrever componentes em Rust, um pouco como no React:
- Leptos (em inglês): um framework moderno e rápido, que também consegue renderizar páginas no servidor.
- Yew (em inglês): um framework maduro, baseado em componentes.
- WebAssembly é um formato binário rápido e seguro que navegadores (e servidores) conseguem rodar; o Rust compila bem para ele porque não tem garbage collector.
- Adicione o target com
rustup target add wasm32-unknown-unknowne instale owasm-pack. - Marque as funções com
#[wasm_bindgen], definacrate-type = ["cdylib", "rlib"]e compile comwasm-pack build --target web. - No JavaScript, faça
await init()primeiro e só depois chame as suas funções; sirva a página porhttp://, e não porfile://. - Números atravessam a fronteira de graça; strings e slices são copiadas. Por isso, faça poucas chamadas, cada uma com bastante trabalho.
- Publique builds de release com compressão e meça antes de confiar em ajustes de tamanho.
Teste o que aprendeu
Seção intitulada “Teste o que aprendeu”Teste o que aprendeu
1. Qual comando adiciona o target do Rust para WebAssembly no navegador?
2. O que o atributo #[wasm_bindgen] faz?
3. A sua página fica parada em "Carregando…" e o console mostra um erro de CORS para um endereço file:///. Qual é a solução?
4. Qual destes é o mais barato de passar do JavaScript para o Rust?
Pontuação: 0 / 4
Para saber mais
Seção intitulada “Para saber mais”- Compiling from Rust to WebAssembly — MDN (em inglês): um tutorial passo a passo que cria e roda um projeto com wasm-pack.
- The wasm-bindgen Guide (em inglês): todos os tipos suportados, como chamar as APIs do navegador e muitos exemplos.
- Documentação do wasm-pack (em inglês): todas as opções do
build, targets e perfis. - wasm32-unknown-unknown — The rustc book (em inglês): o que esse target suporta e quais são os seus limites.
- WebAssembly — MDN: o que é o Wasm e como os navegadores o executam, independentemente do Rust.