WebAssembly in TypeScript
Dieser Artikel erklärt WebAssembly in TypeScript.
Wir erklären praktische und leicht verständliche Methoden zur Integration von TypeScript und WebAssembly.
YouTube Video
WebAssembly in TypeScript
WebAssembly (Wasm) ist eine Laufzeitumgebung im Binärformat, die nahezu mit nativer Geschwindigkeit im Browser läuft. Durch das Aufrufen von Wasm aus TypeScript können Sie rechenintensive Prozesse und bestehende native Bibliotheken, die in C/C++ oder Rust geschrieben wurden, effizient nutzen.
Grundlegender Ausführungsablauf
Hier erklären wir den grundlegenden Ausführungsablauf von Wasm. TypeScript (oder der Browser) lädt die .wasm-Datei, instanziiert sie und ruft die exportierten Funktionen auf.
-
- Erstellen Sie eine .wasm-Binärdatei mit AssemblyScript, Rust, C++ oder verwenden Sie eine bereits vorhandene.
-
- Laden Sie die .wasm-Datei in TypeScript (oder im Browser) und instanziieren Sie sie synchron oder asynchron.
-
- Rufen Sie die exportierten Funktionen auf und teilen Sie den Speicher bei Bedarf mit
WebAssembly.Memory.
- Rufen Sie die exportierten Funktionen auf und teilen Sie den Speicher bei Bedarf mit
WebAssembly.instantiateStreaming
Als Nächstes zeigen wir ein grundlegendes Beispiel, wie eine Wasm-Datei geladen und eine exportierte Funktion aufgerufen wird. Der Browser muss instantiateStreaming unterstützen.
Der folgende Code ist ein Beispiel dafür, wie simple.wasm vom Server abgerufen und die Funktion add aufgerufen wird.
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);- Funktionen innerhalb von Wasm werden in
instance.exportsgespeichert. - Da TypeScript keine Typinformationen erhält, müssen Sie
@ts-ignoreverwenden oder eigene Typdefinitionen erstellen.
Arbeitsablauf mit AssemblyScript
AssemblyScript erlaubt es, Wasm in einer TypeScript-ähnlichen Syntax zu schreiben und ist daher besonders für TypeScript-Entwickler geeignet. Hier erstellen wir eine einfache Funktion, kompilieren sie zu .wasm und .d.ts und rufen sie aus TypeScript auf.
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}- Mit dem Befehl
asc(dem AssemblyScript-Compiler) können Sie eine.wasm-Datei und optional eine Typdefinitionsdatei.d.tserzeugen. Um es lokal auszuprobieren, installieren Sieassemblyscriptmit npm und bauen Sie das Projekt.
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 ist ein Beispiel, wie Sie den Aufruf und das Laden von der TypeScript-Seite aus durchführen.
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 erfordert eine sorgfältige Handhabung von Speicher-Modellen und Zeichenketten, ist aber für grundlegende numerische Berechnungen sehr einfach zu verwenden.
Rust + wasm-bindgen (Eine leistungsstarke und weit verbreitete Option)
In diesem Abschnitt wird erläutert, wie man Wasm in Rust schreibt und mit JavaScript oder TypeScript mithilfe von wasm-bindgen verbindet. Hier verwenden wir eine einfache Fibonacci-Funktion als Beispiel, um zu zeigen, wie man das generierte Modul als ES-Modul importiert.
Exportieren Sie die Funktionen aus Rust mit Hilfe von 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}- Wenn Sie mit
wasm-packoder demwasm-bindgen-CLI bauen, werden Typdefinitionen für TypeScript und JS-Wrapper generiert, sodass Sie sie direkt als ESM importieren können.
1# build with wasm-pack
2# install wasm-pack from https://drager.github.io/wasm-pack/installer/
3wasm-pack build --target nodejs --out-dir pkgImportieren und verwenden Sie das ES-Modul aus pkg auf der TypeScript-Seite.
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-packerzeugt JavaScript-Wrapper und.d.ts-Typdefinitionen, sodass die Verwendung aus TypeScript heraus sehr einfach ist. Bitte beachten Sie, dass beim Angeben vonwebfür die Option--targetdes Befehlswasm-packeine asynchrone Initialisierung erforderlich ist.
Praxisbeispiel für gemeinsamen Speicher: Übergeben und Verarbeiten von Arrays (Low Level)
Beim Austausch großer Datenmengen mit Wasm ist die gemeinsame Nutzung von ArrayBuffer für einen effizienten Datenaustausch wichtig. Hier zeigen wir ein Beispiel mit AssemblyScript, aber das gleiche Prinzip gilt auch für wasm-bindgen in Rust.
Bereiten Sie auf der AssemblyScript-Seite eine exportierte Funktion zum Schreiben in den Speicher vor. Zum Beispiel würde eine Funktion, die jedes Element eines Arrays quadriert, folgendermaßen aussehen.
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}Um die von AssemblyScript verwendeten Speichereinstellungen festzulegen, bereiten Sie die folgende asconfig.json vor.
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- Um diese Funktion aufzurufen, müssen Sie den
ArrayBufferin den Wasm-Speicher kopieren und den Zeiger übergeben.
Nachfolgend finden Sie ein Beispiel dafür, wie Sie mit WebAssembly.Memory in TypeScript Daten kopieren und die Funktion aufrufen.
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.bufferist der gemeinsam genutzte lineare Speicher; das Minimieren von Kopiervorgängen verbessert die Verarbeitungsgeschwindigkeit so weit wie möglich. Beachten Sie auch, dass Zeiger sich auf Positionen in Bytes beziehen, währendTypedArrays nach Elementanzahl verwaltet werden – achten Sie also darauf, diese Unterschiede nicht zu verwechseln.
Typsichere Handhabung: TypeScript-Typdefinitionen vorbereiten
Wasm-Exporte sind JavaScript-Objekte, daher erleichtert das Bereitstellen von Typdefinitionen auf der TypeScript-Seite die Entwicklung. Hier ein einfaches Beispiel für eine Typdefinitionsdatei.
Das Folgende zeigt die minimale Typdefinition, die Sie manuell als simple.d.ts erstellen können.
1// simple.d.ts
2export function add(a: number, b: number): number;
3export const memory: WebAssembly.Memory;- Wenn Sie dies in die
typeRootsIhrertsconfig.jsonlegen oderdeclare moduleverwenden, wird die Typprüfung aktiviert.wasm-packerzeugt praktischerweise.d.ts-Dateien automatisch, daher empfiehlt es sich, diese zu verwenden.
Initialisierungsmuster zur Laufzeit: Synchron vs. Asynchron
Da Wasm-Module I/O (Laden) und Kompilierung erfordern, ist eine asynchrone Initialisierung üblich. Es gibt jedoch auch ein Muster, bei dem Sie das WebAssembly.Module im Voraus zwischenspeichern und synchron instanziieren.
Nachfolgend die grundlegende Code-Struktur zur asynchronen Initialisierung von WebAssembly. In realen Projekten wird dieses Muster empfohlen.
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}- Eine asynchrone Initialisierung erleichtert die flexible Einbindung von Fehlerbehandlung und Lazy Loading und ist daher in der praktischen Entwicklung sehr zu empfehlen. Außerdem enthält der von
wasm-packgenerierte Code eineinit()-API zur Initialisierung; es ist also hilfreich, sich an diesen Ablauf zu gewöhnen.
Praktische Überlegungen zur Performance
Hier einige Aspekte, die Sie für deutliche Leistungssteigerungen beachten sollten. Beachten Sie diese Optimierungstipps, wenn Sie TypeScript und WebAssembly kombinieren.
- Wenn Funktionsaufrufe sehr häufig erfolgen, kann der Overhead beim Aufruf zwischen JavaScript und Wasm zum Engpass werden. Wir empfehlen, Daten möglichst zu stapeln und in einem Durchgang zu verarbeiten.
- Speicherzuweisung und Kopiervorgänge erhöhen die Verarbeitungsbelastung. Nutzen Sie gemeinsame Buffer und Zeiger, um diese Vorgänge zu minimieren.
- Seien Sie vorsichtig beim Umgang mit Fließkommazahlen. In TypeScript werden sie zum Typ
number, aber Sie können sie genau behandeln, indem Sie die Typen auf der Wasm-Seite angleichen.
Zusammenfassung
Durch die Kombination von TypeScript und WebAssembly können Sie nahezu native Performance im Browser erreichen. Dies ist besonders effektiv bei rechenintensiven Aufgaben oder wenn Sie vorhandene native Assets nutzen möchten. Diese Kombination ist eine sehr leistungsstarke Option, wenn Sie die Performance Ihrer Webanwendung verbessern möchten.
Sie können den obigen Artikel mit Visual Studio Code auf unserem YouTube-Kanal verfolgen. Bitte schauen Sie sich auch den YouTube-Kanal an.