WebAssembly em TypeScript

WebAssembly em TypeScript

Este artigo explica WebAssembly em TypeScript.

Explicaremos métodos práticos e fáceis de entender para integrar TypeScript e WebAssembly.

YouTube Video

WebAssembly em TypeScript

WebAssembly (Wasm) é um runtime em formato binário que roda em velocidade quase nativa dentro do navegador. Ao chamar Wasm a partir do TypeScript, você pode utilizar processos intensivos em computação e bibliotecas nativas existentes escritas em C/C++ ou Rust de forma eficiente.

Fluxo Básico de Execução

Aqui, explicaremos o fluxo básico de execução do Wasm. O TypeScript (ou o navegador) busca o arquivo .wasm, instancia e chama as funções exportadas.

    1. Crie um binário .wasm usando AssemblyScript, Rust, C++ ou utilize um já existente.
    1. Busque o arquivo .wasm no TypeScript (ou no navegador) e instancie-o de forma síncrona ou assíncrona.
    1. Chame as funções exportadas e compartilhe a memória usando WebAssembly.Memory, se necessário.

WebAssembly.instantiateStreaming

A seguir, mostraremos um exemplo básico de como carregar um arquivo Wasm e chamar uma função exportada. O navegador precisa suportar instantiateStreaming.

O código a seguir é um exemplo de como buscar simple.wasm do servidor e chamar a função add.

 1// TypeScript: load-and-call.ts
 2// Fetch and instantiate a wasm module and call its exported `add` function.
 3async function run() {
 4  const response = await fetch('http://localhost:3000/simple.wasm');
 5  // Use instantiateStreaming when available for efficiency.
 6  const { instance } = await WebAssembly.instantiateStreaming(response, {});
 7  // @ts-ignore
 8  const result = instance.exports.add(2, 3);
 9  console.log('2 + 3 =', result);
10}
11run().catch(console.error);
  • As funções dentro do Wasm são armazenadas em instance.exports.
  • Como o TypeScript não recebe informações de tipo, você precisa usar @ts-ignore ou criar suas próprias definições de tipo.

Fluxo de Trabalho Usando AssemblyScript

AssemblyScript permite escrever Wasm com uma sintaxe semelhante ao TypeScript, tornando-o uma escolha acessível para desenvolvedores TypeScript. Aqui, preparamos uma função simples, compilamos em .wasm e .d.ts, e a chamamos a partir do TypeScript.

1// assembly/index.ts (AssemblyScript)
2// npm install --save-dev assemblyscript
3
4// Export a simple function to add two integers.
5export function add(a: i32, b: i32): i32 {
6  return a + b;
7}
  • Usando asc (o compilador do AssemblyScript), você pode gerar um arquivo .wasm e, opcionalmente, um arquivo de definição de tipos .d.ts. Para testar localmente, instale assemblyscript com npm e faça o build.
1# build commands
2# npm install --save-dev assemblyscript
3npx asc assembly/index.ts   -o build/simple.wasm   -t build/simple.wat   --bindings esm   --exportTable   --sourceMap
4
5# optionally generate d.ts with --exportRuntime or use as-bind / loader tools

Aqui está um exemplo de como buscar e chamar pelo lado do TypeScript.

1// ts client that loads AssemblyScript-generated wasm
2async function runAssemblyScript() {
3  const res = await fetch('http://localhost:3000/build/simple.wasm');
4  const { instance } = await WebAssembly.instantiateStreaming(res, {});
5  // @ts-ignore
6  console.log('AssemblyScript add:', instance.exports.add(10, 7));
7}
8runAssemblyScript().catch(console.error);
  • O AssemblyScript exige cuidado ao lidar com modelos de memória e strings, mas é muito fácil de usar para cálculos numéricos básicos.

Rust + wasm-bindgen (Uma opção poderosa e muito utilizada)

Esta seção explica o fluxo de trabalho de escrever Wasm em Rust e integrá-lo com JavaScript ou TypeScript usando o wasm-bindgen. Aqui, usamos uma função simples de Fibonacci como exemplo para demonstrar como importar o módulo gerado como um módulo ES.

Exporte funções do lado Rust usando wasm-bindgen.

 1// src/lib.rs (Rust)
 2// install wasm-pack from https://drager.github.io/wasm-pack/installer/
 3use wasm_bindgen::prelude::*;
 4
 5// Export a function to JavaScript using wasm-bindgen.
 6#[wasm_bindgen]
 7pub fn fib(n: u32) -> u32 {
 8    if n <= 1 { return n; }
 9    let mut a = 0;
10    let mut b = 1;
11    for _ in 2..=n {
12        let tmp = a + b;
13        a = b;
14        b = tmp;
15    }
16    b
17}
  • Ao compilar com wasm-pack ou a CLI do wasm-bindgen, são geradas definições de tipo para TypeScript e wrappers JS, permitindo importá-los diretamente como ESM.
1# build with wasm-pack
2# install wasm-pack from https://drager.github.io/wasm-pack/installer/
3wasm-pack build --target nodejs --out-dir pkg

No lado TypeScript, importe e utilize o módulo ES do diretório pkg.

 1// Node.js: import WASM module built with --target web
 2// import init, { fib } from '../pkg/my_wasm_module.js';
 3// Node.js: import WASM module built with --target nodejs
 4import wasm from '../pkg/my_wasm_module.js';
 5
 6async function run() {
 7  //await init();   // --target web
 8  console.log('fib(10)=', wasm.fib(10));
 9}
10
11run().catch(console.error);
  • O wasm-pack gera wrappers JavaScript e definições de tipo .d.ts, tornando fácil o uso a partir do TypeScript. Por favor, note que ao especificar web para a opção --target do comando wasm-pack, a inicialização assíncrona é necessária.

Exemplo real de compartilhamento de memória: passagem e processamento de arrays (baixo nível)

Ao trocar grandes quantidades de dados com Wasm, é importante compartilhar o ArrayBuffer para uma troca de dados eficiente. Aqui mostramos um exemplo usando AssemblyScript, mas o mesmo princípio se aplica com o wasm-bindgen do Rust.

No lado AssemblyScript, prepare uma função exportada para escrita na memória. Por exemplo, uma função para elevar cada elemento de um array ao quadrado ficaria assim.

 1// assembly/array_ops.ts (AssemblyScript)
 2// Square values in place in the wasm linear memory starting at `ptr` for `len` elements.
 3export function square_in_place(ptr: usize, len: i32): void {
 4  // Treat memory as a pointer to 32-bit integers.
 5  for (let i = 0; i < len; i++) {
 6    let offset = ptr + (i << 2); // i * 4 bytes
 7    let value = load<i32>(offset);
 8    store<i32>(offset, value * value);
 9  }
10}

Para especificar as configurações de memória usadas pelo AssemblyScript, prepare o seguinte asconfig.json.

1{
2  "options": {
3    "memoryBase": 0,
4    "importMemory": false,
5    "initialMemory": 1,
6    "maximumMemory": 10
7  }
8}
1 npx asc assembly/array_ops.ts   -o build/array_ops.wasm   -t build/array_ops.wat   --bindings esm   --exportTable   --sourceMap
  • Para chamar essa função, você precisa copiar o ArrayBuffer para o espaço de memória do Wasm e passar o ponteiro.

Abaixo está um exemplo de como usar WebAssembly.Memory no TypeScript para copiar dados e chamar a função.

 1// TypeScript: use memory to pass array to wasm
 2async function runArrayOps() {
 3  const res = await fetch('http://localhost:3000/build/array_ops.wasm');
 4  const { instance } = await WebAssembly.instantiateStreaming(res, {});
 5  // @ts-ignore
 6  const memory: WebAssembly.Memory = instance.exports.memory;
 7  // Create a view into wasm memory.
 8  const i32View = new Int32Array(memory.buffer);
 9
10  // Example data
11  const input = new Int32Array([1, 2, 3, 4]);
12  // Choose an offset (in i32 elements) to copy data to (simple example: at index 0).
13  const offset = 0;
14  i32View.set(input, offset);
15
16  // Call wasm function: ptr in bytes, len in elements
17  // @ts-ignore
18  instance.exports.square_in_place(offset * 4, input.length);
19
20  // Read back result
21  const result = i32View.slice(offset, offset + input.length);
22  console.log('squared:', result);
23}
24runArrayOps().catch(console.error);
  • memory.buffer é a memória linear compartilhada; minimizar cópias melhora ao máximo a velocidade de processamento. Observe também que ponteiros se referem a posições em bytes, enquanto TypedArrays são geridos por contagem de elementos, então tome cuidado para não confundir essas diferenças.

Tratamento com tipo seguro: prepare definições de tipos TypeScript

As exportações do Wasm são objetos JavaScript, portanto fornecer definições de tipo no lado TypeScript tornará o desenvolvimento mais fácil. Aqui está um exemplo simples de arquivo de definição de tipo.

A seguir mostra uma definição de tipo mínima que você pode criar manualmente como simple.d.ts.

1// simple.d.ts
2export function add(a: number, b: number): number;
3export const memory: WebAssembly.Memory;
  • Colocar isso em typeRoots do seu tsconfig.json ou usar declare module ativará a verificação de tipo. O wasm-pack gera automaticamente arquivos .d.ts, o que é bem útil de usar.

Padrões de inicialização em tempo de execução: síncrona vs assíncrona

Como os módulos Wasm exigem I/O (busca) e compilação, a inicialização assíncrona é comum. No entanto, há também um padrão em que você faz cache do WebAssembly.Module previamente e instancia de forma síncrona.

Abaixo está a estrutura básica do código para inicializar o WebAssembly de forma assíncrona. Em projetos reais, esse padrão é recomendado.

1// async init pattern
2async function initWasm(url: string) {
3  const res = await fetch(url);
4  const { instance, module } = await WebAssembly.instantiateStreaming(res, {});
5  return instance;
6}
  • A inicialização assíncrona facilita a incorporação flexível de tratamento de erros e carregamento preguiçoso, sendo a forma mais conveniente no desenvolvimento prático. Além disso, o código gerado por wasm-pack inclui uma API init() para inicialização, então se acostumar com esse fluxo vai ajudar muito no seu trabalho.

Considerações práticas de desempenho

Aqui estão alguns pontos a considerar para melhorias significativas de desempenho. Consulte estas dicas de otimização ao combinar TypeScript e WebAssembly.

  • Quando as chamadas de função são muito frequentes, a sobrecarga das chamadas entre JavaScript e Wasm pode se tornar um gargalo. Recomendamos agrupar os dados e processá-los de uma só vez sempre que possível.
  • A alocação e a cópia de memória aumentam a carga de processamento. Utilize buffers compartilhados e ponteiros para minimizar essas operações.
  • Tenha cuidado ao lidar com números de ponto flutuante. No TypeScript, eles se tornam do tipo number, mas é possível lidar com eles de forma precisa fazendo corresponder os tipos no lado do Wasm.

Resumo

Ao combinar TypeScript e WebAssembly, você pode alcançar desempenho próximo ao nativo no navegador. Isso é especialmente útil para tarefas intensivas em computação ou quando você deseja aproveitar ativos nativos já existentes. Essa combinação é uma opção muito poderosa quando você deseja melhorar o desempenho do seu aplicativo web.

Você pode acompanhar o artigo acima usando o Visual Studio Code em nosso canal do YouTube. Por favor, confira também o canal do YouTube.

YouTube Video