Введение в продвинутую интеграцию WebAssembly и TypeScript
WebAssembly (Wasm) произвел революцию в веб-разработке, позволив выполнять код на скорости, близкой к нативной, прямо в браузере. Однако на продвинутом уровне разработчики сталкиваются с вызовом: как эффективно сочетать строгую типизацию TypeScript с низкоуровневыми возможностями Wasm? В этой статье мы рассмотрим техники, выходящие за рамки базовых примеров: ручное управление памятью, создание оберток с полной типизацией, оптимизацию вызовов и сборку сложных проектов.
Материал предназначен для разработчиков, уже знакомых с основами Wasm и TypeScript. Мы не будем останавливаться на компиляции "Hello World" — сфокусируемся на реальных задачах: работа с буферами, структурами данных, многопоточностью и профилированием.
1. Продвинутое управление памятью: ручное выделение и структуры
Базовый пример экспорта функции из Wasm в TypeScript обычно работает с примитивами. На продвинутом уровне вам потребуется передавать сложные данные: строки, массивы структур, изображения. Рассмотрим, как это делается через WebAssembly.Memory.
1.1. Выделение и освобождение памяти
Предположим, у нас есть модуль Wasm, написанный на AssemblyScript или C, который экспортирует функции malloc и free. На TypeScript мы создаем обертку:
// types.ts
export interface WasmMemory {
malloc(size: number): number;
free(ptr: number): void;
memory: WebAssembly.Memory;
}
// memory-manager.ts
export 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 для нуль-терминатора
const view = new Uint8Array(this.wasm.memory.buffer);
view.set(bytes, ptr);
view[ptr + bytes.length] = 0; // нуль-терминатор
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);
}
}
Этот подход гарантирует, что память не утекает, а TypeScript полностью контролирует жизненный цикл данных.
1.2. Работа со структурами данных
Для передачи объектов (например, точки с координатами {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)
};
}
}
Важно: всегда проверяйте выравнивание памяти. Wasm требует выравнивания по 4 байта для f32.
2. Типизация импортов и экспортов Wasm в TypeScript
Без типизации работать с Wasm опасно: можно передать число вместо указателя и получить segfault. Используйте строгие интерфейсы.
2.1. Генерация типов из Wasm
Инструмент wasm-objdump или wasm2wat позволяет получить сигнатуры функций. Для проекта на AssemblyScript используйте генератор типов:
// wasm-types.d.ts
declare namespace WasmModule {
interface Exports {
add(a: number, b: number): number;
processImage(ptr: number, len: number): number;
readonly memory: WebAssembly.Memory;
}
interface Imports {
env: {
log: (ptr: number) => void;
now: () => number;
};
}
}
// loader.ts
export async function loadWasm(): Promise<WasmModule.Exports> {
const importObj: WasmModule.Imports = {
env: {
log: (ptr: number) => {
const str = readStringFromMemory(ptr);
console.log(str);
},
now: () => Date.now()
}
};
const result = await WebAssembly.instantiateStreaming(
fetch('module.wasm'),
importObj
);
return result.instance.exports as unknown as WasmModule.Exports;
}
Благодаря этому, TypeScript будет проверять типы аргументов и возвращаемых значений на этапе компиляции.
2.2. Безопасная работа с указателями
Создайте абстракцию "умного указателя", которая автоматически освобождает память:
class WasmPtr<T> {
private ptr: number;
private manager: MemoryManager;
private destructor: (ptr: number) => void;
constructor(ptr: number, manager: MemoryManager, destructor: (ptr: number) => void) {
this.ptr = ptr;
this.manager = manager;
this.destructor = destructor;
}
get pointer(): number { return this.ptr; }
free(): void {
if (this.ptr !== 0) {
this.destructor(this.ptr);
this.ptr = 0;
}
}
// Шаблон для чтения данных через функцию
read<R>(reader: (ptr: number) => R): R {
return reader(this.ptr);
}
}
// Использование
const ptr = new WasmPtr(allocatedPointer, manager, (p) => wasm.free(p));
const point = ptr.read((p) => pointManager.readPoint(p));
ptr.free(); // автоматически вызываем free
3. Оптимизация производительности: inline-вызовы и асинхронность
На продвинутом уровне критична скорость. Рассмотрим два подхода.
3.1. Inline-функции и минимизация накладных расходов
Каждый вызов Wasm из TypeScript имеет накладные расходы (контекстный переход). Для критичных участков кода объединяйте несколько операций в одну Wasm-функцию:
// Вместо:
// const a = wasm.add(x, y);
// const b = wasm.multiply(a, z);
// Созда