WebAssembly in TypeScript
Dit artikel legt WebAssembly in TypeScript uit.
We leggen praktische en makkelijk te begrijpen methodes uit om TypeScript en WebAssembly te integreren.
YouTube Video
WebAssembly in TypeScript
WebAssembly (Wasm) is een binaire runtime die bijna met native snelheid in de browser draait. Door Wasm aan te roepen vanuit TypeScript kun je efficiënt rekenintensieve processen en bestaande native bibliotheken geschreven in C/C++ of Rust gebruiken.
Basisuitvoeringsstroom
Hier leggen we de basisuitvoeringsstroom van Wasm uit. TypeScript (of de browser) haalt het .wasm-bestand op, initialiseert het en roept geëxporteerde functies aan.
-
- Maak een .wasm-binary met AssemblyScript, Rust, C++ of gebruik een bestaand bestand.
-
- Haal het .wasm-bestand op in TypeScript (of de browser) en initialiseert het synchroon of asynchroon.
-
- Roep de geëxporteerde functies aan en deel het geheugen via
WebAssembly.Memoryindien nodig.
- Roep de geëxporteerde functies aan en deel het geheugen via
WebAssembly.instantiateStreaming
Vervolgens laten we een basisevoorbeeld zien van het laden van een Wasm-bestand en het aanroepen van een geëxporteerde functie. De browser moet instantiateStreaming ondersteunen.
De volgende code is een voorbeeld van het ophalen van simple.wasm van de server en het aanroepen van de functie 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);- Functies binnen Wasm worden opgeslagen in
instance.exports. - Omdat TypeScript geen type-informatie ontvangt, moet je
@ts-ignoregebruiken of eigen typedefinities aanmaken.
Workflow met AssemblyScript
AssemblyScript stelt je in staat om Wasm te schrijven in een syntax vergelijkbaar met TypeScript, wat het aantrekkelijk maakt voor TypeScript ontwikkelaars. Hier bereiden we een eenvoudige functie voor, bouwen deze naar .wasm en .d.ts, en roepen hem aan vanuit 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}- Door gebruik te maken van
asc(deAssemblyScriptcompiler), kun je een.wasm-bestand genereren en optioneel een type-definitie.d.ts-bestand. Om het lokaal te proberen, installeer jeassemblyscriptmet npm en bouw je het.
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 toolsHier volgt een voorbeeld van ophalen en aanroepen vanuit de TypeScript-kant.
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 vereist zorgvuldige omgang met geheugenmodellen en strings, maar is erg eenvoudig voor simpele numerieke berekeningen.
Rust + wasm-bindgen (Een krachtige en vaak gebruikte optie)
In deze sectie wordt uitgelegd hoe je Wasm schrijft in Rust en het verbindt met JavaScript of TypeScript met behulp van wasm-bindgen. Hier gebruiken we een eenvoudige Fibonacci-functie als voorbeeld om te laten zien hoe je de gegenereerde module als een ES-module importeert.
Exporteer functies aan de Rust-kant met 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}- Als je bouwt met
wasm-packof dewasm-bindgenCLI, worden type-definities voor TypeScript en JS-wrappers gecreëerd, waardoor je ze direct als ESM kunt importeren.
1# build with wasm-pack
2# install wasm-pack from https://drager.github.io/wasm-pack/installer/
3wasm-pack build --target nodejs --out-dir pkgAan de TypeScript-kant importeer je en gebruik je de ES-module vanuit 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-packgenereert JavaScript-wrappers en.d.tstype-definities, waardoor het eenvoudig is vanuit TypeScript te gebruiken. Houd er rekening mee dat wanneer jewebopgeeft voor de--target-optie van hetwasm-pack-commando, asynchrone initialisatie vereist is.
Praktisch voorbeeld van geheugen delen: Arrays doorgeven en verwerken (laag niveau)
Bij het uitwisselen van grote hoeveelheden data met Wasm is het delen van ArrayBuffer belangrijk voor efficiënte dataoverdracht. Hier tonen we een voorbeeld met AssemblyScript, maar hetzelfde principe geldt voor Rust’s wasm-bindgen.
Aan de AssemblyScript-kant bereid je een geëxporteerde functie voor om naar het geheugen te schrijven. Bijvoorbeeld, een functie om elk element van een array te kwadrateren zou er zo uitzien.
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}Om de geheugensinstellingen te specificeren die door AssemblyScript worden gebruikt, maak je het volgende asconfig.json-bestand aan.
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- Om deze functie aan te roepen, moet je de
ArrayBuffernaar het Wasm-geheugen kopiëren en de pointer doorgeven.
Hieronder is een voorbeeld van het gebruik van WebAssembly.Memory in TypeScript om data te kopiëren en de functie aan te roepen.
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.bufferis het gedeelde lineaire geheugen; zo min mogelijk kopiëren verbetert de verwerkingssnelheid maximaal. Let ook op dat pointers naar byte-posities verwijzen, terwijlTypedArrays door elementaantal worden beheerd. Let erop deze verschillen niet te verwarren.
Typesafe afhandeling: Maak TypeScript typedefinities aan
Wasm-exports zijn JavaScript-objecten; typdefinities aanbieden aan de TypeScript-kant maakt ontwikkeling makkelijker. Hier is een simpel voorbeeld van een type-definitiebestand.
Hieronder staat de minimale typedefinitie die je handmatig kunt maken als simple.d.ts.
1// simple.d.ts
2export function add(a: number, b: number): number;
3export const memory: WebAssembly.Memory;- Door dit in de
typeRootsvan jetsconfig.jsonte plaatsen ofdeclare modulete gebruiken, wordt type-checking mogelijk.wasm-packgenereert gemakkelijk automatisch.d.ts-bestanden, dus het is handig om deze te gebruiken.
Initialisatiepatronen tijdens runtime: Synchronous versus Asynchronous
Omdat Wasm-modules I/O (ophalen) en compilatie vereisen, is asynchrone initialisatie gebruikelijk. Er is echter ook een patroon waarbij je WebAssembly.Module van tevoren in de cache plaatst en het synchroon initialiseert.
Hieronder staat de basisstructuur om WebAssembly asynchroon te initialiseren. In echte projecten wordt dit patroon aanbevolen.
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}- Asynchrone initialisatie maakt het gemakkelijk om foutafhandeling en lazy loading flexibel in te voeren; het is het handigst in praktijk. Bovendien bevat de door
wasm-packgegenereerde code eeninit()-API voor initialisatie, dus gewend raken aan deze workflow helpt je werk soepeler te laten verlopen.
Praktische prestatie-overwegingen
Hier zijn enkele punten om in gedachten te houden voor aanzienlijke prestatieverbetering. Raadpleeg deze optimalisatietips bij het combineren van TypeScript en WebAssembly.
- Wanneer functieroproepen zeer frequent zijn, kan de overhead van het aanroepen tussen JavaScript en Wasm een knelpunt worden. Wij raden aan om data te groeperen en zoveel mogelijk in één keer te verwerken.
- Geheugenallocatie en kopiëren vergroten de verwerkingslast. Gebruik gedeelde buffers en pointers om deze bewerkingen te minimaliseren.
- Wees voorzichtig bij het werken met ‘floating point’-getallen. In TypeScript worden ze van het type
number, maar je kunt ze nauwkeuriger verwerken door de types aan de Wasm-kant aan te passen.
Samenvatting
Door TypeScript en WebAssembly te combineren, kun je bijna native prestaties behalen in de browser. Dit is vooral effectief voor rekenintensieve taken of wanneer je bestaande native code wilt benutten. Deze combinatie is een zeer krachtige optie wanneer je de prestaties van je webapplicatie wilt verbeteren.
Je kunt het bovenstaande artikel volgen met Visual Studio Code op ons YouTube-kanaal. Bekijk ook het YouTube-kanaal.