Modelo do WebAssembly no Rust
WebAssembly (WASM) é um formato de bytecode portátil e com sandbox, e Rust é uma das poucas linguagens de sistemas que pode ter como alvo com quase nenhuma alteração no código da lógica principal. Essa combinação é o motivo pelo qual Rust aparece com tanta frequência em analisadores do lado do navegador, codecs e motores de jogos: ele obtém desempenho próximo ao nativo dentro de um limite de segurança projetado para código não confiável. Entender o modelo desta página é importante porque WASM não é "Rust executando no navegador" da maneira que as pessoas costumam supor - é Rust compilado para uma máquina completamente diferente, uma sem sistema operacional, sem heap a menos que você traga uma, e sem linha direta para o DOM a menos que algo construa a ponte para você.
Esta página é a âncora conceitual para o restante da seção WebAssembly. A
página Noções Básicas mostra os comandos e o boilerplate; esta página explica por que esses
comandos existem e o que está realmente acontecendo sob o wasm-bindgen, o
alvo wasm32-unknown-unknown e a alternativa WASI.
Resumo
- WebAssembly é um conjunto de instruções virtuais baseado em pilha para o qual o Rust compila através de um backend LLVM, e ele é executado dentro de um sandbox que não tem acesso ambiente ao sistema host.
- Por que é Importante: O modelo de propriedade do Rust produz código rápido e previsível sem um coletor de lixo, que é exatamente o perfil que uma VM com sandbox recompensa - mas o sandbox também remove suposições que o código Rust normalmente faz sobre memória, threads e I/O.
- Conceitos Chave: memória linear, o alvo wasm32, a fronteira host/guest, a geração de cola wasm-bindgen, JsValue e WASI.
- Quando Usar: Código de navegador com uso intensivo de computação (análise, processamento de imagem/áudio, criptografia), sistemas de plugins portáteis e execução com sandbox no lado do servidor onde você precisa de velocidade próxima à nativa com forte isolamento.
- Limitações / Compromissos: Sem acesso direto ao DOM ou chamadas de sistema sem cola gerada, cada travessia de fronteira custa uma cópia ou serialização, e a toolchain (bindgen, wasm-pack, wasm-opt) adiciona uma etapa de compilação que os desenvolvedores Rust que visam plataformas nativas não têm.
- Tópicos Relacionados: sandboxing de memória linear, interfaces de função estrangeira, o Modelo de Componentes WASI, padrões de interoperabilidade JavaScript.
Fundamentos
WebAssembly não é um conjunto de instruções de CPU como x86-64 ou ARM64. É um
bytecode baseado em pilha projetado para ser verificado rapidamente e executado por uma VM host -
o motor JS de um navegador, Wasmtime, Wasmer ou um runtime de borda. Quando você compila Rust
"para WebAssembly", o compilador não está produzindo código de máquina para o chip do seu laptop;
está produzindo bytecode .wasm que outro programa carregará, validará e executará. Esse programa host está fazendo o trabalho real de transformar bytecode em
instruções que sua CPU executa, geralmente via JIT.
Rust atinge este formato através de um de seus alvos wasm32. Os dois que você encontrará mais são
wasm32-unknown-unknown, que não assume nenhum sistema operacional, e wasm32-wasi (e seu sucessor wasm32-wasip2), que assume uma interface de sistema estreita e baseada em capacidade chamada WASI. "Unknown-unknown" é um nome direto, mas honesto: fornecedor desconhecido, SO desconhecido. Não há sistema de arquivos, nem sockets, nem threads, nem std::env a menos que algo os conecte explicitamente. Esta é a maior mudança mental para um desenvolvedor Rust vindo de alvos nativos ou de servidor - a maior parte do que std normalmente faz por você assume um SO por baixo, e o alvo padrão do WASM não tem nenhum.
O modelo de memória reforça o mesmo ponto. Um módulo WASM possui um bloco contíguo chamado
memória linear - conceitualmente um grande Vec<u8> que o módulo pode ler,
escrever e crescer, mas que o host também pode inspecionar de fora. O próprio alocador do Rust fica
em cima dessa memória linear exatamente como ficaria em cima de um heap nativo, então Box, Vec e String
funcionam como esperado dentro do código Rust. O que não é transferido automaticamente é qualquer noção de ponteiros serem significativos para qualquer coisa que esteja do outro lado da parede do sandbox. Um &str é real e seguro dentro do Rust; para JavaScript, é apenas um deslocamento e um comprimento em um array de bytes que ele não pode interpretar sem ajuda.
Uma analogia simples: pense em um módulo WASM como um contêiner de transporte trancado, não como um convidado em sua casa. Nada entra ou sai, exceto pelas portas com as quais o contêiner foi construído. O código Rust dentro desse contêiner se comporta normalmente - verificação de empréstimo, propriedade, panics, tudo funciona exatamente como funciona nativamente - mas nada fora do contêiner pode ver ou tocar nesse mundo sem uma porta construída para o propósito. wasm-bindgen é a ferramenta que constrói essas portas.
Mecânicas e Interações
A fronteira host/guest é a ideia organizadora para todo o resto nesta seção. O "guest" é seu módulo WASM compilado; o "host" é o que o incorpora e o chama, mais comumente um runtime JavaScript em um navegador. A comunicação através dessa fronteira é deliberadamente estreita: a própria especificação WASM permite apenas passar números (inteiros e de ponto flutuante) como argumentos e valores de retorno de função. Strings, structs, arrays e objetos JS não são conceitos nativos do WASM - eles precisam ser codificados em números (ponteiros e comprimentos na memória linear) e decodificados novamente do outro lado.
É isso que wasm-bindgen automatiza. Não é uma ponte de tempo de execução que intercepta chamadas no tempo de execução; é um gerador de código que é executado no tempo de compilação e produz duas metades correspondentes: cola do lado do Rust que empacota/desempacota valores na memória linear, e um arquivo .js do lado JavaScript que sabe como ler essa memória
e apresentá-la como valores JS normais (strings, objetos, classes). Quando você anota uma função com #[wasm_bindgen], você está pedindo ao macro para gerar esse código de marshaling para você em vez de escrever manualmente aritmética de ponteiros. wasm-pack então empacota o .wasm compilado mais os arquivos .js/.d.ts gerados em algo que se parece e importa como um módulo npm normal.
Todo valor que cruza essa fronteira é copiado ou representado através de
JsValue, um identificador opaco que o Rust mantém e que na verdade vive em uma tabela do lado JavaScript. Passar uma String do Rust para o JS copia bytes da memória linear para uma nova string JS; passar um objeto JS para o Rust o envolve como um JsValue que o Rust não pode inspecionar diretamente sem cola adicional (js_sys, serde-wasm-bindgen). É por isso que o código de interoperação que cruza a fronteira com frequência - uma pequena chamada por evento DOM, por exemplo - tende a se tornar o gargalo real em aplicativos WASM, mesmo que a computação Rust em si seja rápida. O custo não é computação; é
tradução.
Uma breve ilustração do que o macro está substituindo conceitualmente:
// O que #[wasm_bindgen] gera cola para, conceitualmente:
// JS chama greet(ptr, len) com um par ponteiro/comprimento na memória linear.
// Rust lê esses bytes de volta como um &str antes de executar a lógica real.
#[no_mangle]
pub extern "C" fn greet(ptr: *const u8, len: usize) -> *mut u8 {
let name = unsafe {
std::str::from_utf8_unchecked(std::slice::from_raw_parts(ptr, len))
};
let out = format!("Hello, {name}!");
Box::into_raw(out.into_boxed_str().into()) as *mut u8
}wasm-bindgen escreve algo assim para você, mais o JavaScript correspondente que
aloca memória, escreve os bytes da string e lê o resultado de volta - para que você
nunca toque em ponteiros brutos no código de aplicação comum.
A outra consequência do modelo de sandbox é o que desaparece. Threads, no sentido clássico de SO, não existem a menos que o host opte por SharedArrayBuffer e um crate como
wasm-bindgen-rayon o conecte; por padrão, um módulo WASM é de thread única. Panics não desenrolam em um rastreamento de pilha que o navegador entende por padrão também - um panic Rust não tratado trava toda a instância, que é por que console_error_panic_hook existe como uma camada de tradução, não um recurso Rust. E chamadas de sistema - acesso a arquivos, rede, variáveis de ambiente - simplesmente não estão lá sob wasm32-unknown-unknown; WASI existe especificamente para fornecer uma
versão padronizada e com escopo de capacidade dessas chamadas de sistema de volta para hosts não-navegador
(CLIs, funções de borda, runtimes de plugins) sem reintroduzir confiança total do SO.
Considerações Avançadas e Aplicações
O modelo de sandboxing é um recurso de segurança, não um acidente da toolchain. Um módulo WASM só pode afetar o mundo através das exportações que o host escolheu chamar e das importações que o host escolheu fornecer - não há autoridade ambiente, que é por que WASM (e especialmente o modelo de capacidade do WASI) é usado para sistemas de plugins onde o autor do plugin não é totalmente confiável. Esta é a mesma propriedade que torna a depuração mais difícil: um crash dentro do sandbox não pode corromper a memória do host, mas também não pode dar a você um rastreamento de pilha nativo familiar sem suporte de ferramenta deliberado.
O desempenho neste modelo tem uma forma específica. Uma vez que o código está dentro do sandbox e
executando, o WASM compilado do Rust está próximo da velocidade nativa para trabalho limitado por CPU: sem pausas de GC,
sem sobrecarga de despacho dinâmico além do que você escreveu, e as otimizações usuais do LLVM ainda
se aplicam antes mesmo que o backend wasm32 seja executado. A sobrecarga vive
quase inteiramente na fronteira e na inicialização do módulo - análise/validação do
bytecode .wasm, instanciação da memória linear e cada chamada marshalada. É por isso que
a otimização de tamanho e a frequência de travessia de fronteira dominam os conselhos práticos nesta
seção muito mais do que "tornar o código Rust em si mais rápido".
O ecossistema também tem avançado além da cola wasm-bindgen escrita à mão em direção ao
Modelo de Componentes WASI, que define interfaces (WIT) que podem descrever tipos mais ricos
- registros, variantes, recursos - sem que cada linguagem reinvente sua própria
ponte específica para JS.
wasm-bindgencontinua sendo a escolha pragmática para alvos de navegador/JS especificamente; o Modelo de Componentes visa um futuro mais amplo e independente de host, onde módulos WASM se compõem diretamente uns com os outros, não apenas com JavaScript.
| Abordagem | Força | Fraqueza | Melhor Ajuste |
|---|---|---|---|
wasm-bindgen + wasm32-unknown-unknown | Interop JS madura, ferramentas npm ricas via wasm-pack | Específico para navegador/JS; chamadas de fronteira têm custo de marshaling | Aplicativos de navegador chamando APIs JS diretamente |
wasm32-wasi / wasip2 | Chamadas de sistema padronizadas, portáteis entre hosts não-navegador | Suporte assíncrono e de threading ainda amadurecendo | CLIs, funções de borda, plugins com sandbox no lado do servidor |
Exportações brutas extern "C", sem bindgen | Cola menor possível, sem JS gerado | Você implementa manualmente todo o marshaling e gerenciamento de memória | Incorporações mínimas com um host personalizado, não JS |
Conceitos Errôneos Comuns
- "WASM faz o Rust rodar no navegador como um aplicativo nativo." - Ele roda dentro de um sandbox estrito sem acesso ambiente ao DOM, sistema de arquivos ou SO; cada capacidade precisa ser explicitamente conectada através de cola gerada ou WASI.
- "wasm-bindgen é um runtime que intercepta chamadas." - É um gerador de código no tempo de compilação; quando seu módulo é executado, a cola é apenas Rust compilado comum e JS lendo/escrevendo memória linear.
- "Passar dados pela fronteira é basicamente gratuito, como uma chamada de função." - Valores não numéricos são copiados ou serializados na memória linear a cada travessia, razão pela qual APIs de travessia de fronteira muito ativas se tornam o verdadeiro gargalo.
- "As garantias de propriedade e empréstimo do Rust se estendem ao lado JS." - Elas só valem dentro do módulo WASM; uma vez que um valor cruza para o território
JsValue, o mundo de referência mutável e coletado por lixo do JS assume o controle. - "wasm32-unknown-unknown e wasm32-wasi são basicamente o mesmo alvo com nomes diferentes." - Eles representam contratos de host fundamentalmente diferentes: um não assume nada, o outro assume uma interface de sistema definida e com escopo de capacidade.
- "Um panic Rust em WASM se comporta como um panic em qualquer outro lugar." - Por padrão, ele trava toda a instância do módulo com um erro opaco; diagnósticos legíveis exigem um hook de panic explícito.
FAQs
O que "compilar para WebAssembly" realmente produz?
Um binário .wasm contendo bytecode baseado em pilha mais uma tabela de exportações/importações, não código de máquina nativo para sua CPU. Um runtime host carrega, valida e executa.
Por que a biblioteca std do Rust não funciona imediatamente no wasm32-unknown-unknown?
A maior parte do std assume um sistema operacional por baixo para coisas como arquivos,
threads e variáveis de ambiente. wasm32-unknown-unknown não fornece nada disso,
então esses caminhos std são ou substituídos por stubs ou indisponíveis.
O wasm-bindgen faz parte da especificação WebAssembly?
Não. WebAssembly em si só suporta a passagem de números através de limites de função.
wasm-bindgen é uma ferramenta do ecossistema Rust que gera o código de marshaling necessário para
apresentar tipos JS mais ricos sobre essa base puramente numérica.
Como os dados realmente vão de uma String Rust para uma string JavaScript?
O lado Rust escreve os bytes da string na memória linear e retorna um par ponteiro/comprimento; a cola JS gerada lê esses bytes de volta do buffer de memória do módulo e os decodifica em uma string JS nativa. É uma cópia, não um compartilhamento.
Por que minha função WASM é mais lenta do que o esperado, mesmo que a lógica Rust seja simples?
O custo geralmente está na travessia da fronteira, não na computação. Cada chamada que passa dados não triviais paga um custo de marshaling; agrupar trabalho em chamadas menores e mais frequentes é a correção padrão.
Módulos WASM podem acessar o sistema de arquivos ou a rede diretamente?
Não sob wasm32-unknown-unknown. Sob WASI, eles podem, mas apenas através de
capacidades explicitamente concedidas (como diretórios pré-abertos), nunca através de acesso ambiente ao SO
como um binário nativo tem.
Threads funcionam em WASM compilado com Rust?
Não por padrão; um módulo é de thread única, a menos que o host suporte
SharedArrayBuffer e a compilação opte por um crate de threading como
wasm-bindgen-rayon.
O que acontece quando o código Rust entra em pânico dentro de um módulo WASM?
A instância trava e se torna inutilizável sem um hook instalado. Builds de desenvolvimento
geralmente instalam console_error_panic_hook para traduzir o trap em um erro legível no console do navegador.
O wasm32-wasi é a mesma coisa que rodar Rust nativamente, apenas com sandbox?
Conceitualmente próximo, mas não idêntico: WASI expõe um subconjunto de chamadas de sistema baseado em capacidade, não a superfície POSIX completa, então o código que assume acesso arbitrário ao SO ainda pode falhar mesmo quando compila.
Quando devo escolher o Modelo de Componentes em vez do wasm-bindgen?
Quando você precisa que módulos WASM interoperem uns com os outros ou com hosts não-JS usando tipos compartilhados mais ricos, em vez de fazer a ponte especificamente para um ambiente JavaScript.
Propriedade e empréstimo ainda importam quando o código tem como alvo WASM?
Sim, totalmente, dentro do próprio módulo Rust. O compilador impõe as mesmas regras de sempre; o que muda é que essas garantias param na fronteira; o outro lado é o modelo de memória do JS, não o do Rust.
Por que builds WASM precisam de uma etapa de otimização separada como wasm-opt?
O backend wasm32 do LLVM otimiza razoavelmente bem, mas wasm-opt aplica
passes específicos de WASM (do toolkit Binaryen) no binário já compilado,
capturando ganhos de tamanho e contagem de instruções que o compilador Rust não visa diretamente.
Relacionados
- Noções Básicas de WebAssembly - um guia prático sobre alvos, compilações e a toolchain que esta página explica conceitualmente
- wasm-bindgen - a cola gerada e o mapeamento de tipos na fronteira JS/Rust na prática
- WASI - a interface de sistema baseada em capacidade para hosts WASM não-navegador
- Desempenho e Tamanho - por que as travessias de fronteira e o tamanho do binário dominam o desempenho do WASM no mundo real
- Componentes WASM e o Modelo de Componentes - a evolução independente de host além da cola bindgen escrita à mão
Versões da Stack: Esta página foi escrita para Rust 1.97.0 (edição 2024), Tokio 1.x, Axum 0.8, serde 1.0, sqlx 0.8, clap 4 e Polars 0.46+.