WebAssembly в TypeScript

WebAssembly в TypeScript

В этой статье объясняется использование WebAssembly в TypeScript.

Мы расскажем о практичных и понятных способах интеграции TypeScript и WebAssembly.

YouTube Video

WebAssembly в TypeScript

WebAssembly (Wasm) — это среда выполнения в бинарном формате, работающая в браузере с почти нативной скоростью. Вызывая Wasm из TypeScript, вы можете эффективно использовать ресурсоёмкие вычисления и существующие нативные библиотеки, написанные на C/C++ или Rust.

Базовый поток выполнения

Здесь мы объясним основной поток выполнения Wasm. TypeScript (или браузер) загружает файл .wasm, инициализирует его и вызывает экспортированные функции.

    1. Создайте бинарный файл .wasm с помощью AssemblyScript, Rust, C++ или используйте уже готовый.
    1. Загрузите файл .wasm в TypeScript (или в браузере) и инициализируйте его синхронно или асинхронно.
    1. Вызывайте экспортированные функции и используйте совместную память через WebAssembly.Memory при необходимости.

WebAssembly.instantiateStreaming

Далее мы приведем базовый пример загрузки файла Wasm и вызова экспортированной функции. Браузер должен поддерживать instantiateStreaming.

Следующий код является примером получения файла simple.wasm с сервера и вызова функции 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);
  • Функции внутри Wasm хранятся в instance.exports.
  • Поскольку TypeScript не получает информации о типах, вам потребуется использовать @ts-ignore или создать собственные определения типов.

Рабочий процесс с использованием AssemblyScript

AssemblyScript позволяет писать Wasm с синтаксисом, похожим на TypeScript, что делает его удобным для разработчиков TypeScript. Здесь мы подготавливаем простую функцию, компилируем её в .wasm и .d.ts, и вызываем из 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}
  • С помощью asc (компилятора AssemblyScript) вы можете сгенерировать файл .wasm и, при необходимости, файл определения типов .d.ts. Для локального тестирования установите assemblyscript через npm и соберите проект.
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 tools

Вот пример загрузки и вызова с помощью 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 требует осторожного обращения с моделями памяти и строками, но очень прост в использовании для базовых числовых вычислений.

Rust + wasm-bindgen (мощный и широко используемый вариант)

В этом разделе объясняется рабочий процесс написания Wasm на Rust и связывания его с JavaScript или TypeScript с помощью wasm-bindgen. Здесь мы используем простую функцию Фибоначчи как пример, чтобы показать, как импортировать сгенерированный модуль в формате ES.

Экспортируйте функции на стороне Rust с помощью 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}
  • При сборке с помощью wasm-pack или CLI wasm-bindgen создаются типовые определения для TypeScript и JS-оболочки, которые можно напрямую импортировать как 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 pkg

На стороне TypeScript импортируйте и используйте ES-модуль из директории 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-pack генерирует JS-оболочки и файлы определения типов .d.ts, благодаря чему использование из TypeScript становится простым. Обратите внимание, что при указании web для параметра --target команды wasm-pack требуется асинхронная инициализация.

Практический пример обмена памятью: передача и обработка массивов (низкоуровневый подход)

При передаче большого объёма данных в Wasm важно использовать совместное использование ArrayBuffer для эффективного обмена. Здесь приведён пример с использованием AssemblyScript, но тот же принцип применим и к wasm-bindgen в Rust.

На стороне AssemblyScript подготовьте экспортированную функцию для записи в память. Например, функция, возводящая в квадрат каждый элемент массива, может выглядеть следующим образом.

 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}

Чтобы задать параметры памяти, используемые в AssemblyScript, подготовьте следующий файл 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
  • Чтобы вызвать эту функцию, вам нужно скопировать ArrayBuffer в память Wasm и передать указатель.

Ниже приведен пример использования WebAssembly.Memory в TypeScript для копирования данных и вызова функции.

 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 — это общая линейная память; уменьшение числа копирований максимально ускоряет обработку. Обратите внимание: указатели указывают на позицию в байтах, в то время как TypedArray оперирует количеством элементов — не перепутайте эти различия.

Типобезопасное использование: подготовьте определения типов для TypeScript

Экспорты Wasm — это JavaScript-объекты, поэтому создание определений типов на стороне TypeScript упростит разработку. Вот простой пример файла определения типов.

Ниже приведено минимальное определение типа, которое можно создать вручную как simple.d.ts.

1// simple.d.ts
2export function add(a: number, b: number): number;
3export const memory: WebAssembly.Memory;
  • Добавив это в typeRoots вашего tsconfig.json или используя declare module, вы включите проверку типов. wasm-pack автоматически создаёт файлы .d.ts, поэтому их удобно использовать.

Паттерны инициализации во время выполнения: синхронно и асинхронно

Поскольку модули Wasm требуют операций ввода-вывода (fetch) и компиляции, обычно применяется асинхронная инициализация. Однако есть паттерн, при котором вы кэшируете WebAssembly.Module заранее и инициализируете синхронно.

Ниже приведена базовая структура кода для асинхронной инициализации WebAssembly. В реальных проектах этот паттерн рекомендуется использовать.

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}
  • Асинхронная инициализация позволяет гибко реализовать обработку ошибок и ленивую загрузку, поэтому такой способ оптимален на практике. Кроме того, код, сгенерированный wasm-pack, содержит API init() для инициализации, поэтому освоение этого потока ускорит вашу работу.

Практические аспекты производительности

Вот несколько моментов, которые помогут значительно повысить производительность. Воспользуйтесь этими советами по оптимизации при интеграции TypeScript и WebAssembly.

  • Если вызовы функций происходят очень часто, накладные расходы между JavaScript и Wasm могут стать узким местом. Рекомендуется объединять данные в пакеты и обрабатывать их за один проход по максимуму.
  • Выделение памяти и копирование повышают нагрузку на процессор. Используйте совместно используемые буферы и указатели, чтобы минимизировать такие операции.
  • Будьте осторожны при обработке чисел с плавающей точкой. В TypeScript они представлены типом number, но для корректной обработки нужно согласовывать типы на стороне Wasm.

Резюме

Комбинируя TypeScript и WebAssembly, вы можете достичь почти нативной производительности в браузере. Это особенно эффективно для вычислительно-интенсивных задач или при необходимости использовать существующие нативные ресурсы. Эта комбинация — очень мощный вариант для повышения производительности вашего веб-приложения.

Вы можете следовать этой статье, используя Visual Studio Code на нашем YouTube-канале. Пожалуйста, также посмотрите наш YouTube-канал.

YouTube Video