WebAssembly и TypeScript: продвинутые техники интеграции

Онлайн-тренажер Python для начинающих

Изучайте Python легко и без перегрузки теорией. Решайте практические задачи с автоматической проверкой, получайте подсказки на русском языке и пишите код прямо в браузере — без необходимости что-либо устанавливать.

Начать курс

Введение в продвинутую интеграцию 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);

// Созда                

Блоги

Рекомендаций по книгам