WebAssembly i TypeScript

WebAssembly i TypeScript

Denne artikel forklarer WebAssembly i TypeScript.

Vi forklarer praktiske og letforståelige metoder til at integrere TypeScript og WebAssembly.

YouTube Video

WebAssembly i TypeScript

WebAssembly (Wasm) er et runtime i binært format, der kører med næsten indbygget hastighed i browseren. Ved at kalde Wasm fra TypeScript kan du effektivt udnytte computertunge processer og eksisterende native biblioteker skrevet i C/C++ eller Rust.

Grundlæggende eksekveringsflow

Her forklarer vi det grundlæggende eksekveringsflow for Wasm. TypeScript (eller browseren) henter .wasm-filen, instantierer den og kalder eksporterede funktioner.

    1. Opret en .wasm-binær ved hjælp af AssemblyScript, Rust, C++ eller forbered en eksisterende.
    1. Hent .wasm-filen i TypeScript (eller i browseren) og instantier den synkront eller asynkront.
    1. Kald de eksporterede funktioner og del hukommelse ved hjælp af WebAssembly.Memory, hvis det er nødvendigt.

WebAssembly.instantiateStreaming

Dernæst viser vi et grundlæggende eksempel på indlæsning af en Wasm-fil og kald af en eksporteret funktion. Browseren skal understøtte instantiateStreaming.

Følgende kode er et eksempel på at hente simple.wasm fra serveren og kalde funktionen 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);
  • Funktioner i Wasm gemmes i instance.exports.
  • Da TypeScript ikke modtager typeinformation, skal du bruge @ts-ignore eller oprette dine egne typedefinitioner.

Arbejdsgang med AssemblyScript

AssemblyScript gør det muligt at skrive Wasm med syntaks, der ligner TypeScript, hvilket gør det til et tilgængeligt valg for TypeScript-udviklere. Her forbereder vi en simpel funktion, bygger den til .wasm og .d.ts, og kalder den fra 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}
  • Ved at bruge asc (kompileren for AssemblyScript), kan du generere en .wasm-fil og eventuelt også en typedefinitionsfil .d.ts. For at prøve det lokalt, installér assemblyscript med npm og byg det.
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

Her er et eksempel på hentning og kald fra TypeScript-siden.

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);
  • AssemblyScript kræver omhyggelig håndtering af hukommelsesmodeller og strenge, men er meget nemt at bruge til grundlæggende numeriske beregninger.

Rust + wasm-bindgen (En kraftfuld og ofte brugt løsning)

Dette afsnit forklarer arbejdsgangen ved at skrive Wasm i Rust og forbinde det med JavaScript eller TypeScript ved hjælp af wasm-bindgen. Her bruger vi en simpel Fibonacci-funktion som eksempel til at demonstrere, hvordan du importerer det genererede modul som et ES-modul.

Eksporter funktioner fra Rust-siden ved hjælp af 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}
  • Når du bygger med wasm-pack eller wasm-bindgen CLI, genereres typedefinitioner til TypeScript og JS-wrappere, så du kan importere dem direkte som 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

På TypeScript-siden importerer og bruger du ES-modulet fra 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);
  • wasm-pack genererer JavaScript-wrappere og .d.ts-typedefinitioner, hvilket gør det nemt at bruge fra TypeScript. Bemærk venligst, at når du angiver web for --target-muligheden i wasm-pack-kommandoen, kræves asynkron initialisering.

Eksempel fra virkeligheden på deling af hukommelse: Videregivelse og behandling af arrays (lavt niveau)

Når du udveksler store mængder data med Wasm, er det vigtigt at dele ArrayBuffer for effektiv dataudveksling. Her viser vi et eksempel med AssemblyScript, men det samme princip gælder for Rusts wasm-bindgen.

På AssemblyScript-siden forberedes en eksporteret funktion til at skrive til hukommelsen. For eksempel vil en funktion, der kvadrerer hvert enkelt element i et array, se sådan ud.

 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}

For at angive hukommelsesindstillingerne, der bruges af AssemblyScript, forbered følgende 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
  • For at kalde denne funktion skal du kopiere ArrayBuffer ind i Wasm-hukommelsesområdet og videregive pointeren.

Nedenfor er et eksempel på brug af WebAssembly.Memory i TypeScript til at kopiere data og kalde funktionen.

 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 er den delte lineære hukommelse; at minimere kopier øger behandlinghastigheden mest muligt. Bemærk også, at pointers refererer til positioner i bytes, mens TypedArrays håndteres efter elementantal, så vær opmærksom på ikke at forveksle disse forskelle.

Type-sikker håndtering: Forbered TypeScript-typedefinitioner

Wasm-eksporter er JavaScript-objekter, så det gør udviklingen lettere at stille typedefinitioner til rådighed på TypeScript-siden. Her er et simpelt eksempel på en typedefinitions-fil.

Følgende viser den minimale typedefinition, du manuelt kan oprette som simple.d.ts.

1// simple.d.ts
2export function add(a: number, b: number): number;
3export const memory: WebAssembly.Memory;
  • Hvis du placerer dette i typeRoots i din tsconfig.json eller bruger declare module, aktiveres typekontrol. wasm-pack genererer bekvemt .d.ts-filer automatisk, så det er nyttigt at bruge dem.

Initialiseringsmønstre ved runtime: Synkron vs. asynkron

Da Wasm-moduler kræver I/O (hentning) og kompilering, er asynkron initialisering almindelig. Der findes dog også et mønster, hvor du på forhånd cacher WebAssembly.Module og instantierer den synkront.

Nedenfor er den grundlæggende kodestruktur for asynkron initialisering af WebAssembly. I reale projekter anbefales dette mønster.

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}
  • Asynkron initialisering gør det nemt fleksibelt at indarbejde fejlhåndtering og lazy loading, hvilket gør det mest praktisk i virkelig udvikling. Desuden inkluderer koden, der genereres af wasm-pack, en init()-API til initialisering, så det er hjælpsomt at vænne sig til denne arbejdsgang.

Praktiske overvejelser om ydeevne

Her er nogle punkter, man bør være opmærksom på for betydelige ydeevneforbedringer. Referer til disse optimeringstips, når du kombinerer TypeScript og WebAssembly.

  • Når funktionskald er meget hyppige, kan overhead ved kald mellem JavaScript og Wasm blive en flaskehals. Vi anbefaler at samle data og behandle dem samlet så meget som muligt.
  • Hukommelsesallokering og kopiering øger behandlingbelastningen. Brug delte buffere og pointers for at minimere disse operationer.
  • Vær forsigtig ved håndtering af floating-point tal. I TypeScript bliver de typen number, men du kan håndtere dem nøjagtigt ved at matche typerne på Wasm-siden.

Sammendrag

Ved at kombinere TypeScript og WebAssembly kan du opnå næsten native ydeevne i browseren. Dette er særligt effektivt til beregningstunge opgaver eller når du vil udnytte eksisterende native biblioteker. Denne kombination er en meget kraftfuld mulighed, når du ønsker at forbedre ydeevnen for din webapplikation.

Du kan følge med i ovenstående artikel ved hjælp af Visual Studio Code på vores YouTube-kanal. Husk også at tjekke YouTube-kanalen.

YouTube Video