Перейти к основному содержимому

Интеграция WebAssembly с JavaScript BigInt

· 5 мин. чтения
Алон Закай

Функция JS-BigInt-Integration упрощает передачу 64-битных целых чисел между JavaScript и WebAssembly. В этом посте объясняется, что это значит и почему это полезно, включая упрощение работы для разработчиков, ускорение выполнения кода и уменьшение времени сборки.

64-битные целые числа

Числа JavaScript представлены в виде doubles, то есть 64-битных чисел с плавающей точкой. Такое значение может содержать любое 32-битное целое число с полной точностью, но не все 64-битные числа. WebAssembly, с другой стороны, полностью поддерживает 64-битные целые числа типа i64. Проблема возникает при их соединении: если функция Wasm возвращает i64, например, то VM выбрасывает исключение при вызове из JavaScript, что-то вроде:

TypeError: Wasm function signature contains illegal type

Как говорит ошибка, i64 не является легальным типом для JavaScript.

Исторически лучшим решением этой проблемы была «легализация» Wasm. Легализация означает преобразование импорта и экспорта Wasm для использования допустимых типов для JavaScript. На практике это сделало два действия:

  1. Замена параметра 64-битного целого числа на два 32-битных параметра, представляющих младшие и старшие биты соответственно.
  2. Замена возвращаемого значения 64-битного целого числа на 32-битное значение, представляющее младшие биты, и использование 32-битного значения сбоку для старших битов.

Например, рассмотрим этот модуль Wasm:

(module
(func $send_i64 (param $x i64)
..))

Легализация преобразует его в это:

(module
(func $send_i64 (param $x_low i32) (param $x_high i32)
(local $x i64) ;; реальное значение, которое будет использоваться остальным кодом
;; код для объединения $x_low и $x_high в $x
..))

Легализация выполняется со стороны инструментов до того, как она достигает VM, который её запускает. Например, библиотека инструментов Binaryen имеет проход под названием LegalizeJSInterface, который выполняет это преобразование, которое выполняется автоматически в Emscripten, когда это необходимо.

Недостатки легализации

Легализация достаточно хорошо работает во многих случаях, но она имеет недостатки, такие как дополнительная работа по объединению или разделению 32-битных частей в 64-битные значения. Хотя это редко случается на критическом пути, когда это происходит, замедление может быть заметным - позже мы увидим некоторые цифры.

Еще одним неудобством является то, что легализация заметна пользователям, поскольку она изменяет интерфейс между JavaScript и Wasm. Вот пример:

// example.c

#include <stdint.h>

extern void send_i64_to_js(int64_t);

int main() {
send_i64_to_js(0xABCD12345678ULL);
}
// example.js

mergeInto(LibraryManager.library, {
send_i64_to_js: function(value) {
console.log("JS получено: 0x" + value.toString(16));
}
});

Это небольшой C-программа, которая вызывает функцию JavaScript-библиотеки (то есть мы определяем внешнюю функцию C в C и реализуем её в JavaScript как простой и низкоуровневый способ вызова между Wasm и JavaScript). Всё, что делает эта программа, это отправляет i64 в JavaScript, где мы пытаемся напечатать его.

Мы можем собрать это с помощью

emcc example.c --js-library example.js -o out.js

Когда мы запускаем его, мы не получаем то, что ожидали:

node out.js
JS получено: 0x12345678

Мы отправили 0xABCD12345678, но получили только 0x12345678 😔. Что происходит здесь, так это легализация превращает этот i64 в два i32, и наш код просто получил младшие 32 бита, и проигнорировал другой отправленный параметр. Чтобы правильно обработать это, нам нужно сделать что-то вроде:

  // i64 разбито на два 32-битных параметра, “младший” и “старший”.
send_i64_to_js: function(low, high) {
console.log("JS получено: 0x" + high.toString(16) + low.toString(16));
}

Запустив это сейчас, мы получаем

JS получено: 0xabcd12345678

Как видите, с легализацией можно жить. Но это может быть довольно раздражающим!

Решение: BigInt в JavaScript

JavaScript теперь имеет значения BigInt, которые представляют целые числа произвольного размера, так что они могут корректно представлять 64-битные целые числа. Естественно захотеть использовать их для представления i64 из Wasm. Именно это делает функция интеграции JS-BigInt!

Emscripten поддерживает интеграцию Wasm BigInt, которую мы можем использовать для компиляции исходного примера (без каких-либо изменений для легализации), просто добавив -s WASM_BIGINT:

emcc example.c --js-library example.js -o out.js -s WASM_BIGINT

После этого можно запустить (обратите внимание, что в настоящее время нам нужно передать Node.js флаг для включения интеграции BigInt):

node --experimental-wasm-bigint a.out.js
JS получил: 0xabcd12345678

Отлично, именно то, что мы хотели!

И это не только проще, но и быстрее. Как упоминалось ранее, на практике редко встречается, чтобы преобразования i64 происходили в горячей области, но если это происходит, то замедление может быть заметным. Если превратить приведенный выше пример в тест производительности, вызывая send_i64_to_js множество раз, версия с BigInt оказывается на 18% быстрее.

Еще одно преимущество интеграции BigInt заключается в том, что инструментальная цепочка может избежать легализации. Если Emscripten не нужно легализовать, то ему может не понадобиться выполнять какую-либо работу с Wasm, который излучает LLVM, что ускоряет время сборки. Вы можете получить ускорение, если соберете с -s WASM_BIGINT и не предоставите никаких других флагов, требующих изменений. Например, -O0 -s WASM_BIGINT работает (но оптимизированные сборки запускают оптимизатор Binaryen, что важно для размера).

Заключение

Интеграция WebAssembly BigInt была реализована в нескольких браузерах, включая Chrome 85 (выпущен 25-08-2020), так что вы можете попробовать уже сегодня!