WebAssembly в TypeScript
В этой статье объясняется использование WebAssembly в TypeScript.
Мы расскажем о практичных и понятных способах интеграции TypeScript и WebAssembly.
YouTube Video
WebAssembly в TypeScript
WebAssembly (Wasm) — это среда выполнения в бинарном формате, работающая в браузере с почти нативной скоростью. Вызывая Wasm из TypeScript, вы можете эффективно использовать ресурсоёмкие вычисления и существующие нативные библиотеки, написанные на C/C++ или Rust.
Базовый поток выполнения
Здесь мы объясним основной поток выполнения Wasm. TypeScript (или браузер) загружает файл .wasm, инициализирует его и вызывает экспортированные функции.
-
- Создайте бинарный файл .wasm с помощью AssemblyScript, Rust, C++ или используйте уже готовый.
-
- Загрузите файл .wasm в TypeScript (или в браузере) и инициализируйте его синхронно или асинхронно.
-
- Вызывайте экспортированные функции и используйте совместную память через
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или CLIwasm-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, содержит APIinit()для инициализации, поэтому освоение этого потока ускорит вашу работу.
Практические аспекты производительности
Вот несколько моментов, которые помогут значительно повысить производительность. Воспользуйтесь этими советами по оптимизации при интеграции TypeScript и WebAssembly.
- Если вызовы функций происходят очень часто, накладные расходы между JavaScript и Wasm могут стать узким местом. Рекомендуется объединять данные в пакеты и обрабатывать их за один проход по максимуму.
- Выделение памяти и копирование повышают нагрузку на процессор. Используйте совместно используемые буферы и указатели, чтобы минимизировать такие операции.
- Будьте осторожны при обработке чисел с плавающей точкой. В TypeScript они представлены типом
number, но для корректной обработки нужно согласовывать типы на стороне Wasm.
Резюме
Комбинируя TypeScript и WebAssembly, вы можете достичь почти нативной производительности в браузере. Это особенно эффективно для вычислительно-интенсивных задач или при необходимости использовать существующие нативные ресурсы. Эта комбинация — очень мощный вариант для повышения производительности вашего веб-приложения.
Вы можете следовать этой статье, используя Visual Studio Code на нашем YouTube-канале. Пожалуйста, также посмотрите наш YouTube-канал.