Узнайте, как встроить код JavaScript в вашу библиотеку WebAssembly для связи с внешним миром.
При работе над интеграцией WebAssembly с Интернетом вам нужен способ вызова внешних API, таких как веб-API и сторонние библиотеки. Затем вам понадобится способ хранения значений и экземпляров объектов, возвращаемых этими API, а также способ передачи этих сохраненных значений другим API позже. Для асинхронных API вам также может потребоваться дождаться обещаний в синхронном коде C/C++ с помощью Asyncify и прочитать результат после завершения операции.
Emscripten предоставляет несколько инструментов для такого взаимодействия:
-
emscripten::val
для хранения значений JavaScript и работы с ними в C++. -
EM_JS
для встраивания фрагментов JavaScript и привязки их как функций C/C++. -
EM_ASYNC_JS
похож наEM_JS
, но упрощает встраивание асинхронных фрагментов JavaScript. -
EM_ASM
для встраивания коротких фрагментов и их встроенного выполнения без объявления функции. -
--js-library
для сложных сценариев, в которых вы хотите объявить множество функций JavaScript вместе как одну библиотеку.
В этом посте вы узнаете, как использовать их все для аналогичных задач.
emscripten::val класс
Класс emcripten::val
предоставляется Embind. Он может вызывать глобальные API, привязывать значения JavaScript к экземплярам C++ и преобразовывать значения между типами C++ и JavaScript.
Вот как использовать его с .await()
Asyncify для получения и анализа JSON:
#include <emscripten/val.h>
using namespace emscripten;
val fetch_json(const char *url) {
// Get and cache a binding to the global `fetch` API in each thread.
thread_local const val fetch = val::global("fetch");
// Invoke fetch and await the returned `Promise<Response>`.
val response = fetch(url).await();
// Ask to read the response body as JSON and await the returned `Promise<any>`.
val json = response.call<val>("json").await();
// Return the JSON object.
return json;
}
// Example URL.
val example_json = fetch_json("https://httpbin.org/json");
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
Этот код работает хорошо, но выполняет множество промежуточных шагов. Каждая операция над val
должна выполнять следующие шаги:
- Преобразуйте значения C++, переданные в качестве аргументов, в некоторый промежуточный формат.
- Перейдите в JavaScript, прочитайте и преобразуйте аргументы в значения JavaScript.
- Выполните функцию
- Преобразуйте результат из JavaScript в промежуточный формат.
- Верните преобразованный результат в C++, и C++, наконец, прочитает его обратно.
Каждый await()
также должен приостанавливать сторону C++, разматывая весь стек вызовов модуля WebAssembly, возвращаясь к JavaScript, ожидая и восстанавливая стек WebAssembly после завершения операции.
Такой код не требует ничего от C++. Код C++ действует только как драйвер для ряда операций JavaScript. Что, если бы вы могли перенести fetch_json
на JavaScript и одновременно сократить накладные расходы на промежуточные шаги?
Макрос EM_JS
EM_JS macro
позволяет переместить fetch_json
в JavaScript. EM_JS
в Emscripten позволяет объявить функцию C/C++, реализуемую фрагментом JavaScript.
Как и сам WebAssembly, он имеет ограничение на поддержку только числовых аргументов и возвращаемых значений. Чтобы передать любые другие значения, вам необходимо преобразовать их вручную с помощью соответствующих API. Вот несколько примеров.
Для передачи чисел не требуется никакого преобразования:
// Passing numbers, doesn't need any conversion.
EM_JS(int, add_one, (int x), {
return x + 1;
});
int x = add_one(41);
При передаче строк в JavaScript и обратно вам необходимо использовать соответствующие функции преобразования и выделения из preamble.js :
EM_JS(void, log_string, (const char *msg), {
console.log(UTF8ToString(msg));
});
EM_JS(const char *, get_input, (), {
let str = document.getElementById('myinput').value;
// Returns heap-allocated string.
// C/C++ code is responsible for calling `free` once unused.
return allocate(intArrayFromString(str), 'i8', ALLOC_NORMAL);
});
Наконец, для более сложных, произвольных типов значений вы можете использовать API JavaScript для ранее упомянутого класса val
. С его помощью вы можете конвертировать значения JavaScript и классы C++ в промежуточные дескрипторы и обратно:
EM_JS(void, log_value, (EM_VAL val_handle), {
let value = Emval.toValue(val_handle);
console.log(value);
});
EM_JS(EM_VAL, find_myinput, (), {
let input = document.getElementById('myinput');
return Emval.toHandle(input);
});
val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
log_value(obj.as_handle()); // logs { x: 1, y: 2 }
val myinput = val::take_ownership(find_input());
// Now you can store the `find_myinput` DOM element for as long as you like, and access it later like:
std::string value = input["value"].as<std::string>();
Учитывая эти API, пример fetch_json
можно было бы переписать, чтобы выполнять большую часть работы, не выходя из JavaScript:
EM_JS(EM_VAL, fetch_json, (const char *url), {
return Asyncify.handleAsync(async () => {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
У нас по-прежнему есть пара явных преобразований в точках входа и выхода функции, но остальное теперь представляет собой обычный код JavaScript. В отличие от эквивалента val
, теперь его можно оптимизировать с помощью механизма JavaScript, и для всех асинхронных операций требуется только один раз приостановить выполнение стороны C++.
Макрос EM_ASYNC_JS
Единственное, что осталось некрасиво, — это оболочка Asyncify.handleAsync
— ее единственная цель — разрешить выполнение async
функций JavaScript с помощью Asyncify. Фактически, этот вариант использования настолько распространен, что теперь существует специализированный макрос EM_ASYNC_JS
, который объединяет их вместе.
Вот как вы можете использовать его для создания окончательной версии примера fetch
:
EM_ASYNC_JS(EM_VAL, fetch_json, (const char *url), {
url = UTF8ToString(url);
// Invoke fetch and await the returned `Promise<Response>`.
let response = await fetch(url);
// Ask to read the response body as JSON and await the returned `Promise<any>`.
let json = await response.json();
// Convert JSON into a handle and return it.
return Emval.toHandle(json);
});
// Example URL.
val example_json = val::take_ownership(fetch_json("https://httpbin.org/json"));
// Now we can extract fields, e.g.
std::string author = json["slideshow"]["author"].as<std::string>();
EM_ASM
EM_JS
— рекомендуемый способ объявления фрагментов JavaScript. Это эффективно, поскольку связывает объявленные фрагменты напрямую, как и любой другой импорт функций JavaScript. Он также обеспечивает хорошую эргономику, позволяя явно объявлять все типы и имена параметров.
Однако в некоторых случаях вы хотите вставить быстрый фрагмент для вызова console.log
, debugger;
оператор или что-то подобное и не хочу заморачиваться с объявлением целой отдельной функции. В таких редких случаях EM_ASM macros family
( EM_ASM
, EM_ASM_INT
и EM_ASM_DOUBLE
) может быть более простым выбором. Эти макросы похожи на макрос EM_JS
, но они выполняют код внутри места вставки, а не определяют функцию.
Поскольку они не объявляют прототип функции, им нужен другой способ указания типа возвращаемого значения и доступа к аргументам.
Вам необходимо использовать правильное имя макроса, чтобы выбрать тип возвращаемого значения. Ожидается, что блоки EM_ASM
будут действовать как функции void
, блоки EM_ASM_INT
могут возвращать целочисленное значение, а блоки EM_ASM_DOUBLE
соответственно возвращают числа с плавающей запятой.
Любые переданные аргументы будут доступны под именами $0
, $1
и т. д. в теле JavaScript. Как и в случае с EM_JS
или WebAssembly в целом, аргументы ограничены только числовыми значениями — целыми числами, числами с плавающей запятой, указателями и дескрипторами.
Вот пример того, как вы можете использовать макрос EM_ASM
для регистрации произвольного значения JS на консоли:
val obj = val::object();
obj.set("x", 1);
obj.set("y", 2);
// executes inline immediately
EM_ASM({
// convert handle passed under $0 into a JavaScript value
let obj = Emval.fromHandle($0);
console.log(obj); // logs { x: 1, y: 2 }
}, obj.as_handle());
--js-библиотека
Наконец, Emscripten поддерживает объявление кода JavaScript в отдельном файле в формате собственной библиотеки :
mergeInto(LibraryManager.library, {
log_value: function (val_handle) {
let value = Emval.toValue(val_handle);
console.log(value);
}
});
Затем вам нужно вручную объявить соответствующие прототипы на стороне C++:
extern "C" void log_value(EM_VAL val_handle);
После объявления с обеих сторон библиотеку JavaScript можно связать вместе с основным кодом с помощью --js-library option
, соединяя прототипы с соответствующими реализациями JavaScript.
Однако этот формат модуля нестандартен и требует тщательного аннотирования зависимостей. Таким образом, он в основном зарезервирован для продвинутых сценариев.
Заключение
В этом посте мы рассмотрели различные способы интеграции кода JavaScript в C++ при работе с WebAssembly.
Включение таких фрагментов позволяет выражать длинные последовательности операций более чистым и эффективным способом, а также использовать сторонние библиотеки, новые API-интерфейсы JavaScript и даже функции синтаксиса JavaScript, которые еще невозможно выразить с помощью C++ или Embind.