Einführung in die fortgeschrittene Integration von WebAssembly und TypeScript
WebAssembly (Wasm) hat die Webentwicklung revolutioniert, indem es die Ausführung von Code mit nahezu nativer Geschwindigkeit direkt im Browser ermöglicht. Auf fortgeschrittenem Niveau stehen Entwickler jedoch vor der Herausforderung: Wie kombiniert man die strenge Typisierung von TypeScript effektiv mit den Low-Level-Fähigkeiten von Wasm? In diesem Artikel betrachten wir Techniken, die über grundlegende Beispiele hinausgehen: manuelle Speicherverwaltung, Erstellung vollständig typisierter Wrapper, Aufrufoptimierung und Builds komplexer Projekte.
Das Material richtet sich an Entwickler, die bereits mit den Grundlagen von Wasm und TypeScript vertraut sind. Wir werden nicht bei der Kompilierung von "Hello World" stehen bleiben – wir konzentrieren uns auf reale Aufgaben: Arbeit mit Puffern, Datenstrukturen, Multithreading und Profiling.
1. Fortgeschrittene Speicherverwaltung: Manuelle Allokation und Strukturen
Ein einfaches Beispiel für den Export einer Funktion von Wasm nach TypeScript arbeitet normalerweise mit primitiven Datentypen. Auf fortgeschrittenem Niveau müssen Sie komplexe Daten übergeben: Zeichenketten, Arrays von Strukturen, Bilder. Betrachten wir, wie dies über WebAssembly.Memory erfolgt.
1.1. Speicher allokieren und freigeben
Angenommen, wir haben ein Wasm-Modul, geschrieben in AssemblyScript oder C, das die Funktionen malloc und free exportiert. In TypeScript erstellen wir einen Wrapper:
// types.tsexport interface WasmMemory { malloc(size: number): number; free(ptr: number): void; memory: WebAssembly.Memory;}
// memory-manager.tsexport class MemoryManager { private wasm: WasmMemory;
constructor(wasm: WasmMemory) { this.wasm = wasm; }
allocateString(str: string): number { const encoder = new TextEncoder(); const bytes = encoder.encode(str); const ptr = this.wasm.malloc(bytes.length + 1); // +1 für Nullterminator const view = new Uint8Array(this.wasm.memory.buffer); view.set(bytes, ptr); view[ptr + bytes.length] = 0; // Nullterminator return ptr; }
readString(ptr: number): string { const view = new Uint8Array(this.wasm.memory.buffer); let end = ptr; while (view[end] !== 0) end++; const bytes = view.slice(ptr, end); return new TextDecoder().decode(bytes); }
freeString(ptr: number): void { this.wasm.free(ptr); }}Dieser Ansatz stellt sicher, dass kein Speicher verloren geht und TypeScript den Lebenszyklus der Daten vollständig kontrolliert.
1.2. Arbeiten mit Datenstrukturen
Verwenden Sie für die Übergabe von Objekten (z. B. eines Punktes mit Koordinaten {x: f32, y: f32}) DataView:
interface Point { x: number; y: number; }
export class PointManager { private wasm: WasmMemory; private readonly SIZE = 8; // 2 float32
constructor(wasm: WasmMemory) { this.wasm = wasm; }
createPoint(p: Point): number { const ptr = this.wasm.malloc(this.SIZE); const view = new DataView(this.wasm.memory.buffer); view.setFloat32(ptr, p.x, true); view.setFloat32(ptr + 4, p.y, true); return ptr; }
readPoint(ptr: number): Point { const view = new DataView(this.wasm.memory.buffer); return { x: view.getFloat32(ptr, true), y: view.getFloat32(ptr + 4, true) }; }}Wichtig: Überprüfen Sie immer die Speicherausrichtung. Wasm erfordert eine 4-Byte-Ausrichtung für f32.
2. Typisierung von Wasm-Importen und -Exporten in TypeScript
Ohne Typisierung ist die Arbeit mit Wasm gefährlich: Man kann eine Zahl anstelle eines Zeigers übergeben und einen Segfault erhalten. Verwenden Sie strenge Interfaces.
2.1. Generierung von Typen aus Wasm
Das Tool wasm-objdump oder wasm2wat ermöglicht es, die Funktionssignaturen zu erhalten. Für ein AssemblyScript-Projekt verwenden Sie einen Typgenerator:
// wasm-types.d.tsdeclare namespace WasmModule { interface Exports { add(a: number, b: number): numb