了解如何在 WebAssembly 库中嵌入 JavaScript 代码以与外界通信。
在处理 WebAssembly 与 Web 的集成时,您需要一种方法来调用 Web API 和第三方库等外部 API。然后,您需要一种方法来存储这些 API 返回的值和对象实例,以及之后将这些存储的值传递给其他 API。对于异步 API,您可能还需要使用 Asyncify 在同步 C/C++ 代码中等待 promise,并在操作完成后读取结果。
Emscripten 提供了多种用于进行此类互动的工具:
emscripten::val
,用于在 C++ 中存储和操作 JavaScript 值。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 类型之间转换值。
下面展示了如何将其与 Asyncify 的 .await()
搭配使用,以便提取和解析某些 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()
还必须通过展开 WebAssembly 模块的整个调用堆栈、返回 JavaScript、等待,并在操作完成后恢复 WebAssembly 堆栈来暂停 C++ 端。
此类代码不需要 C++ 中的任何内容。C++ 代码仅充当一系列 JavaScript 操作的驱动程序。如果您可以将 fetch_json
迁移到 JavaScript 并同时减少中间步骤的开销,结果会怎样?
EM_JS 宏
借助 EM_JS macro
,您可以将 fetch_json
移至 JavaScript。通过 Emscripten 中的 EM_JS
,您可以声明由 JavaScript 代码段实现的 C/C++ 函数。
与 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 传递字符串以及从 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);
});
最后,对于更复杂的任意值类型,您可以使用 JavaScript API 实现前面提到的 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
封装容器,它的唯一目的是允许使用 Asyncify 执行 async
JavaScript 函数。事实上,这种用例非常常见,以至于现在有专门的 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
块会相应地返回浮点数。
在 JavaScript 正文中,任何传递的参数都会以 $0
、$1
等名称可用。与一般的 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-library
最后,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);
在双方代码间进行声明后,即可通过 --js-library option
将 JavaScript 库与主代码相关联,从而将原型与相应的 JavaScript 实现连接起来。
不过,此模块格式不是标准格式,需要仔细进行依赖项注解。因此,它主要用于高级场景。
总结
在这篇博文中,我们了解了在使用 WebAssembly 时将 JavaScript 代码集成到 C++ 中的各种方法。
包含此类代码段可让您以更简洁、更高效的方式表达长序列的操作,并利用第三方库、新的 JavaScript API 甚至是尚无法通过 C++ 或 Embind 表达的 JavaScript 语法功能。