Pular para o conteúdo

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 .wasm podem ser pequenos.
  • Ótimas ferramentas. O wasm-bindgen escreve para você o código de ligação entre Rust e JavaScript, e o wasm-pack compila 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.

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”:

Terminal window
rustup target add wasm32-unknown-unknown

Em 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:

Terminal window
# Instalador para Linux / macOS, do site do wasm-pack
curl https://wasm-bindgen.github.io/wasm-pack/installer/init.sh -sSf | sh
# ou, em qualquer sistema com Rust instalado
cargo install wasm-pack --locked

Um módulo Wasm é uma biblioteca, não um programa com main. Crie uma:

Terminal window
cargo new --lib hello-wasm
cd hello-wasm

Depois 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.

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 = 5
▶ Experimente você mesmo

Quando 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.)

Na pasta do crate, rode:

Terminal window
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.json

O 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.

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:

Terminal window
python3 -m http.server 8080

Abra http://localhost:8080 no navegador. Quando fizemos isso, a página mostrou:

Olá, Ferris! Esta string foi criada em Rust. 2 + 3 = 5

Essa 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]
▶ Experimente você mesmo

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.

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 --dev gerou um arquivo .wasm de 58.591 bytes; o build de release padrão ficou com 21.530 bytes. (Isso na nossa máquina, com o wasm-opt desligado; 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 otimizador wasm-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 = true
codegen-units = 1

Seja 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.

Alguns exemplos, cada um com o relato da própria empresa (em inglês):

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.

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-unknown e instale o wasm-pack.
  • Marque as funções com #[wasm_bindgen], defina crate-type = ["cdylib", "rlib"] e compile com wasm-pack build --target web.
  • No JavaScript, faça await init() primeiro e só depois chame as suas funções; sirva a página por http://, e não por file://.
  • 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

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