WebAssembly e WASI
O NAPI-RS pode compilar um addon para
wasm32-wasip1-threads
e gerar loaders para Node.js e navegadores. Os principais casos de uso são:
- um fallback portátil quando nenhum addon nativo pré-compilado corresponde ao host;
- uma demo em navegador, StackBlitz ou WebContainer da mesma API Rust;
- um pacote direcionado explicitamente a WASI.
Atualmente, wasm32-wasip1-threads é o padrão suportado. Alvos WASI de nível
mais baixo, como wasm32-unknown-unknown, e alvos WASI sem threads exigem que
você adapte threading e dependências por conta própria e não são gerados por
este fluxo.
WARNING
Uma build WASI não é automaticamente equivalente a um addon nativo. APIs do sistema operacional, dependências nativas em C/C++, comportamento do sistema de arquivos, threads, limites de memória e suporte do runtime hospedeiro podem ser diferentes. Teste o artefato WASI como um alvo de release separado.
Início rápido
O ponto de partida mais fácil é napi new com o alvo WASI habilitado. Para um
projeto existente, instale o alvo Rust e adicione-o a napi.targets:
rustup target add wasm32-wasip1-threads
{
"name": "@scope/my-addon",
"main": "index.js",
"types": "index.d.ts",
"browser": "browser.js",
"napi": {
"binaryName": "my-addon",
"targets": [
"x86_64-unknown-linux-gnu",
"aarch64-apple-darwin",
"x86_64-pc-windows-msvc",
"wasm32-wasip1-threads"
],
"wasm": {
"initialMemory": 4000,
"maximumMemory": 65536,
"browser": {
"fs": false,
"asyncInit": false,
"buffer": false,
"errorEvent": true
}
}
}
}
O projeto precisa ter dependências @emnapi/core e @emnapi/runtime
compatíveis. Os scaffolds criados pelas versões atuais de napi new já as
incluem. Se as versões resolvidas pelo projeto forem diferentes da versão de
emnapi usada pela CLI, a build será interrompida com um erro explícito de
incompatibilidade de versão; atualize tudo em conjunto em vez de contornar essa
verificação.
Compile o alvo WASI:
napi build --platform --release --target wasm32-wasip1-threads
Nenhuma flag de cross-compilation é necessária. Crates escritos apenas em Rust
usam o linker WASI do próprio Rust. WASI_SDK_PATH só é necessário quando um
código C/C++ na árvore de dependências exige uma toolchain C para WASI.
Artefatos gerados
Para binaryName: "my-addon", a build cria:
| Arquivo | Finalidade |
|---|---|
my-addon.wasm32-wasi.wasm |
Módulo WASM de release com stripping |
my-addon.wasm32-wasi.debug.wasm |
Módulo que mantém informações de debug/nome quando a geração tem sucesso |
my-addon.wasi.cjs |
Loader WASI para Node.js |
my-addon.wasi-browser.js |
Loader ESM para navegador |
wasi-worker.mjs |
Worker Node usado por threads do emnapi |
wasi-worker-browser.mjs |
Worker de navegador usado por threads do emnapi |
browser.js |
Entrada browser do pacote que reexporta o pacote de plataforma WASI |
index.js e index.d.ts |
Loader normal de seleção de plataforma e tipos compartilhados |
Mantenha cada loader junto com seus arquivos de worker e WASM. Renomear ou mover um único arquivo sem regenerar o loader quebra suas URLs relativas.
Como funciona o fallback de nativo para WASI
O loader normal index.js primeiro tenta o binário nativo da plataforma atual.
Se o carregamento nativo falhar, ele tenta:
- um
my-addon.wasi.cjslocal; - o pacote
@scope/my-addon-wasm32-wasipublicado separadamente.
Use NAPI_RS_FORCE_WASI para testar o fallback mesmo em um host nativo
suportado. Para loaders gerados por @napi-rs/cli 3.7 ou mais recente:
| Valor | Comportamento |
|---|---|
| não definido ou qualquer outra string | Prefere nativo; só tenta WASI depois que o nativo falha |
true |
Tenta e seleciona WASI mesmo se o nativo carregou; sem uma asserção estrita para WASI ausente |
error |
Tenta WASI e lança erro se não existir um binding WASI local ou empacotado |
Valores como 1, 0 e false não forçam WASI. Use error nos testes para
que um artefato ausente não use silenciosamente a implementação nativa:
NAPI_RS_FORCE_WASI=error node ./test.cjs
No Node, o loader WASI gerado:
- usa
NAPI_RS_ASYNC_WORK_POOL_SIZE, depoisUV_THREADPOOL_SIZE, depois4para o pool de async-work do emnapi; - faz preopen da raiz do sistema de arquivos do host por meio da implementação WASI do Node;
- reutiliza e chama
unrefem worker threads para que workers ociosos não mantenham o Node ativo; - prefere o arquivo
.debug.wasmquando ele está presente ao lado do loader.
WARNING
O loader WASI do Node faz preopen da raiz do sistema de arquivos. Trate o addon WASI como código confiável de aplicação nativa, não como um sandbox de segurança para módulos ou entradas não confiáveis.
Demo no navegador
Este transformador de imagens usa
@napi-rs/image por meio de sua
entrada WASI para navegador:
import { Transformer } from '@napi-rs/image'
export async function transform() {
const imageBytes = await fetch(
'https://images-assets.nasa.gov/image/carina_nebula/carina_nebula~orig.png',
).then((res) => res.arrayBuffer())
const transformer = new Transformer(new Uint8Array(imageBytes))
return transformer.webp()
}
Depois de instalar o pacote WASI e configurar o isolamento entre origens, ele
pode ser empacotado com Vite ou
Webpack.
Para uma build local não publicada, importe ./my-addon.wasi-browser.js
diretamente. Para um pacote publicado, a entrada browser da raiz reexporta o
pacote -wasm32-wasi publicado separadamente.
Configuração do servidor no navegador
O alvo com threads usa memória compartilhada do WebAssembly,
SharedArrayBuffer, workers e atomics. Os navegadores expõem
SharedArrayBuffer apenas em uma página com isolamento entre origens por causa
de mitigações de segurança contra side channels:
Several recently-published research articles have demonstrated a new class of timing attacks (Meltdown and Spectre) that work on modern CPUs. Our internal experiments confirm that it is possible to use similar techniques from Web content to read private information between different origins.
Sirva o documento com ambos os headers:
Cross-Origin-Opener-Policy: same-origin
Cross-Origin-Embedder-Policy: require-corp
Por exemplo, no Vite:
import { defineConfig } from 'vite'
export default defineConfig({
server: {
headers: {
'Cross-Origin-Opener-Policy': 'same-origin',
'Cross-Origin-Embedder-Policy': 'require-corp',
},
},
plugins: [
{
name: 'configure-preview-response-headers',
configurePreviewServer(server) {
server.middlewares.use((_req, res, next) => {
res.setHeader('Cross-Origin-Opener-Policy', 'same-origin')
res.setHeader('Cross-Origin-Embedder-Policy', 'require-corp')
next()
})
},
},
],
})
Configure também o CDN/servidor de produção; os headers de desenvolvimento não são levados para o deploy. Scripts, workers, WASM, imagens e fontes cross-origin também precisam atender à política COEP selecionada.
Verifique o resultado no navegador:
console.log(globalThis.crossOriginIsolated) // true
console.log(typeof SharedArrayBuffer) // 'function'
Configuração de runtime no navegador
Os campos napi.wasm controlam o glue de navegador gerado:
| Campo | Padrão | Efeito |
|---|---|---|
initialMemory |
4000 páginas |
Memória compartilhada inicial (uma página WebAssembly tem 64 KiB) |
maximumMemory |
65536 páginas |
Memória compartilhada máxima, 4 GiB |
browser.fs |
false |
Cria um sistema de arquivos em memória, faz preopen de / e exporta __fs / __volume |
browser.asyncInit |
false |
Usa a API de instanciação assíncrona do emnapi |
browser.buffer |
false |
Injeta o Buffer do pacote buffer no contexto do emnapi |
browser.errorEvent |
false |
Encaminha falhas de worker como eventos napi-rs-worker-error na janela |
A entrada de navegador faz fetch do arquivo WASM e, portanto, usa await de
nível superior mesmo quando asyncInit é false. Garanta que o bundler/alvo
de saída suporte workers ESM, import.meta.url e top-level await.
Com fs: true, o acesso ao sistema de arquivos é para o memfs, não para o
sistema de arquivos do host do usuário:
import { __fs } from './my-addon.wasi-browser.js'
__fs.writeFileSync('/input.txt', 'hello')
Com errorEvent: true, observe erros de worker antes de iniciar o trabalho:
window.addEventListener('napi-rs-worker-error', (event) => {
const { detail } = event as CustomEvent<unknown>
console.error('NAPI-RS WASI worker failed', detail)
})
Instale o pacote WebAssembly
Para evitar aumentar toda instalação nativa, o NAPI-RS marca o pacote de
plataforma WASI com cpu: ["wasm32"]. Os gerenciadores de pacotes o ignoram, a
menos que wasm32 seja uma arquitetura de instalação habilitada.
Since we finished the `wasm32-wasi-preview1-threads` target in https://github.com/napi-rs/napi-rs/pull/1669. We need to design a release workflow for wasm package. ## Goals - Automatically fallback to a WASM implementation on platforms where pre-compiling native addons is not supported. - In WebContainer, it can be downloaded and installed automatically, without the need for additional configuration. ## Issues There are three potential implementations. 1. Setting up postinstall scripts for every NAPI-RS package. Detect if the platform is supported, and download the wasm package if not supported. 2. Treat the wasm package as a regular platform-specified package, and set the `os` to a special value like `webcontainer`, so that the package managers can download it on WebContainer automatically. 3. Always distributing wasm package and their dependencies with the package. Unfortunately, these implementations have their own issues. The `postinstall` will not run in the `WebContainer` environment, so setup `postinstall` solution is not perfect for `WebContainer`, beside that, I hate postinstall. Platform-specified package solution need the `WebContainer` host to change some behavior about `process.platform`, it may break some other third-party packages and raise more issues. Always distributing wasm package will increase the download size significantly. There is `308.7kb` bundled runtime JavaScript code besides the wasm file itself.
Yarn
Para o Yarn 4, adicione wasm32 a .yarnrc.yml:
supportedArchitectures:
cpu:
- current
- wasm32
O Yarn 1 não tem uma configuração de arquitetura equivalente mantida. Seu
workaround com --ignore-engines é amplo e ignora outras verificações de
compatibilidade; prefira um gerenciador de pacotes atual para pacotes que
dependem de fallback WASI.
pnpm
supportedArchitectures:
cpu:
- current
- wasm32
npm
As versões atuais do npm suportam uma flag de CPU de destino:
npm install --cpu=wasm32
Após a instalação, verifique o pacote em vez de presumir que a configuração foi respeitada:
npm ls @scope/my-addon-wasm32-wasi
Empacote e publique o WASI
O WASI usa o mesmo fluxo de release com pacote separado que os alvos nativos:
- Inclua
wasm32-wasip1-threadsemnapi.targets. - Compile e teste esse alvo no próprio job de CI.
- Execute
napi create-npm-dirs; o pacote WASI gerado recebecpu: ["wasm32"], uma versão mínima de Node compatível com o loader e suas dependências de emnapi/runtime. - Baixe todos os artefatos dos alvos e execute
napi artifacts. Isso copia o módulo WASM, os loaders de Node/navegador e ambos os workers para o pacote WASI. - Execute testes no Node com
NAPI_RS_FORCE_WASI=errore testes no navegador com isolamento entre origens. - Siga o guia normal de release.
Inspecione a saída final de npm pack --dry-run para o pacote raiz e para o
pacote de plataforma WASI. Este último precisa conter os arquivos .wasm,
.wasi.cjs, .wasi-browser.js e os arquivos de worker.
Compile dependências em C/C++
Se a árvore de dependências compilar C ou C++, instale o
wasi-sdk e defina
WASI_SDK_PATH apontando para a raiz do SDK extraído:
export WASI_SDK_PATH=/absolute/path/to/wasi-sdk
test -x "$WASI_SDK_PATH/bin/clang"
test -x "$WASI_SDK_PATH/bin/wasm-ld"
napi build --platform --release --target wasm32-wasip1-threads
A CLI sempre aponta as variáveis de linker WASI do Cargo para
$WASI_SDK_PATH/bin/wasm-ld. Para as variáveis da toolchain C/C++ (TARGET_CC,
TARGET_CXX, TARGET_AR, TARGET_RANLIB, TARGET_CFLAGS, TARGET_CXXFLAGS e
TARGET_LDFLAGS), um valor já presente no ambiente tem precedência; a CLI
preenche apenas as variáveis não definidas. Mesmo assim, uma dependência ainda
pode ser incompatível se assumir APIs POSIX que o WASI não fornece; uma build
nativa bem-sucedida não é evidência de que a mesma dependência suporte WASI.
Matriz de suporte em runtime
| Host | Status e restrições |
|---|---|
| Node.js | Caminho .wasi.cjs gerado; o pacote de plataforma requer Node 14 ou mais recente e usa APIs WASI/worker do Node |
| Navegador com isolamento entre origens | Caminho gerado com ESM + module worker; requer memória compartilhada, top-level await e serving correto dos assets |
| Navegador sem isolamento | Não suportado para o alvo com threads porque memória compartilhada não está disponível |
| Bun e Deno | Não declare suporte sem um teste de runtime; o carregamento WASI compatível com Node atualmente tem um relato de incompatibilidade em aberto |
| Isolates edge/serverless | Específico de cada host; muitos não expõem as APIs Node WASI, filesystem ou worker esperadas pelo loader gerado |
A limitação de Bun/Deno é acompanhada em napi-rs#2965. Trata-se de uma lacuna de compatibilidade de runtime, não de algo que instruções de instalação de pacote possam corrigir.
Checklist de testes
Antes de publicar um alvo WASI, teste:
- um import simples no Node com
NAPI_RS_FORCE_WASI=error; - erros e operações assíncronas rejeitadas, não apenas funções bem-sucedidas;
- a saída do processo depois que threads e trabalho assíncrono terminam;
- o pacote npm WASI final empacotado/instalado separadamente;
- um servidor de navegador em estilo de produção com headers COOP/COEP;
- inicialização de worker, várias chamadas concorrentes e encaminhamento de erros de worker;
- crescimento de memória no navegador e comportamento de falta de memória com entrada representativa;
- comportamento do memfs quando APIs de filesystem fazem parte do contrato público;
- todo runtime não Node que você listar como suportado.
Para assinaturas de falha e sondas exatas, veja Troubleshooting: WASI failures.