WebAssembly in TypeScript
Questo articolo spiega WebAssembly in TypeScript.
Spiegheremo metodi pratici e facili da comprendere per integrare TypeScript e WebAssembly.
YouTube Video
WebAssembly in TypeScript
WebAssembly (Wasm) è un runtime in formato binario che funziona quasi alla velocità nativa all'interno del browser. Chiamando Wasm da TypeScript, puoi sfruttare in modo efficiente processi computazionalmente intensivi e librerie native esistenti scritte in C/C++ o Rust.
Flusso Base di Esecuzione
Qui spiegheremo il flusso di esecuzione base di Wasm. TypeScript (o il browser) recupera il file .wasm, lo istanzia e chiama le funzioni esportate.
-
- Crea un file binario .wasm usando AssemblyScript, Rust, C++ o prepara uno esistente.
-
- Recupera il file .wasm in TypeScript (o nel browser) e istanzialo in modo sincrono o asincrono.
-
- Chiama le funzioni esportate e condividi la memoria utilizzando
WebAssembly.Memoryse necessario.
- Chiama le funzioni esportate e condividi la memoria utilizzando
WebAssembly.instantiateStreaming
Successivamente, mostreremo un esempio di base per caricare un file Wasm e chiamare una funzione esportata. Il browser deve supportare instantiateStreaming.
Il codice seguente è un esempio di come recuperare simple.wasm dal server e chiamare la funzione 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);- Le funzioni dentro Wasm sono memorizzate in
instance.exports. - Poiché TypeScript non riceve le informazioni sui tipi, devi usare
@ts-ignoreo creare le tue definizioni di tipo.
Flusso di lavoro con AssemblyScript
AssemblyScript ti permette di scrivere Wasm con una sintassi simile a TypeScript, rendendolo una scelta accessibile per gli sviluppatori TypeScript. Qui prepariamo una funzione semplice, la compiliamo in .wasm e .d.ts, e la richiamiamo da 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}- Utilizzando
asc(il compilatore diAssemblyScript), puoi generare un file.wasme, opzionalmente, un file di definizione dei tipi.d.ts. Per provarlo in locale, installaassemblyscriptcon npm e compilalo.
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 toolsEcco un esempio di recupero e chiamata dal lato 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);- AssemblyScript richiede una gestione attenta dei modelli di memoria e delle stringhe, ma è molto facile da usare per calcoli numerici di base.
Rust + wasm-bindgen (Un'opzione potente e comunemente utilizzata)
Questa sezione spiega il flusso di lavoro per scrivere Wasm in Rust e integrarlo con JavaScript o TypeScript usando wasm-bindgen. Qui utilizziamo una semplice funzione Fibonacci come esempio per mostrare come importare il modulo generato come modulo ES.
Esporta le funzioni dal lato 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}- Quando compili con
wasm-packo la CLI diwasm-bindgen, vengono generate le definizioni di tipo per TypeScript e i wrapper JS, consentendoti di importarli direttamente come 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 pkgDal lato TypeScript, importa e usa il modulo ES da 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-packgenera i wrapper JavaScript e le definizioni di tipo.d.ts, rendendo semplice l'utilizzo da TypeScript. Si prega di notare che quando si specificawebper l'opzione--targetdel comandowasm-pack, è necessaria l'inizializzazione asincrona.
Esempio reale di condivisione della memoria: passaggio e elaborazione di array (Basso livello)
Quando si scambiano grandi quantità di dati con Wasm, è importante condividere ArrayBuffer per uno scambio efficiente dei dati. Qui mostriamo un esempio usando AssemblyScript, ma lo stesso principio si applica con wasm-bindgen di Rust.
Dal lato AssemblyScript, prepara una funzione esportata per scrivere in memoria. Ad esempio, una funzione per elevare al quadrato ogni elemento di un array apparirebbe così.
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}Per specificare le impostazioni di memoria utilizzate da AssemblyScript, prepara il seguente 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- Per chiamare questa funzione, devi copiare l'
ArrayBuffernello spazio di memoria Wasm e passare il puntatore.
Di seguito è riportato un esempio che utilizza WebAssembly.Memory in TypeScript per copiare i dati e chiamare la funzione.
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è la memoria lineare condivisa; minimizzare le copie migliora il più possibile la velocità di elaborazione. Nota anche che i puntatori si riferiscono a posizioni in byte, mentre iTypedArraysono gestiti tramite il numero di elementi, quindi fai attenzione a non confondere queste differenze.
Gestione di sicurezza dei tipi: prepara le definizioni di tipo TypeScript
Le esportazioni Wasm sono oggetti JavaScript, quindi fornire definizioni di tipo dal lato TypeScript renderà lo sviluppo più facile. Ecco un semplice esempio di file di definizione di tipo.
Il seguente mostra la definizione di tipo minima che puoi creare manualmente come simple.d.ts.
1// simple.d.ts
2export function add(a: number, b: number): number;
3export const memory: WebAssembly.Memory;- Inserendo questo tra i
typeRootsnel tuotsconfig.jsono usandodeclare moduleabiliterai il controllo dei tipi.wasm-packgenera comodamente file.d.tsautomaticamente, quindi è utile utilizzarli.
Pattern di inizializzazione a runtime: Sincrono vs Asincrono
Poiché i moduli Wasm richiedono I/O (fetch) e compilazione, l'inizializzazione asincrona è comune. Tuttavia, esiste anche un pattern in cui si mette in cache WebAssembly.Module in anticipo e lo si istanzia in modo sincrono.
Di seguito è mostrata la struttura di base del codice per l'inizializzazione asincrona di WebAssembly. Nei progetti reali, questo pattern è raccomandato.
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}- L'inizializzazione asincrona rende facile integrare in modo flessibile la gestione degli errori e il caricamento lazy, rendendola la più conveniente nello sviluppo pratico. Inoltre, il codice generato da
wasm-packinclude un'APIinit()per l'inizializzazione, quindi abituarsi a questo flusso faciliterà il tuo lavoro.
Considerazioni pratiche sulle prestazioni
Ecco alcuni punti da tenere a mente per miglioramenti significativi delle prestazioni. Riferisciti a questi suggerimenti di ottimizzazione quando combini TypeScript e WebAssembly.
- Quando le chiamate di funzione sono molto frequenti, l'overhead delle chiamate tra JavaScript e Wasm può diventare un collo di bottiglia. Si consiglia di raggruppare i dati ed elaborarli tutti insieme per quanto possibile.
- L'allocazione e la copia della memoria aumentano il carico di elaborazione. Utilizza buffer condivisi e puntatori per minimizzare queste operazioni.
- Fai attenzione durante la gestione dei numeri in virgola mobile. In TypeScript diventano di tipo
number, ma puoi gestirli correttamente abbinando i tipi dal lato Wasm.
Riepilogo
Combinando TypeScript e WebAssembly puoi ottenere prestazioni quasi native nel browser. Questo è particolarmente efficace per attività di calcolo intensivo o quando si vogliono sfruttare asset nativi esistenti. Questa combinazione è un'opzione molto potente quando desideri migliorare le prestazioni della tua applicazione web.
Puoi seguire l'articolo sopra utilizzando Visual Studio Code sul nostro canale YouTube. Controlla anche il nostro canale YouTube.