使用 Emscripten 在 C++ 中嵌入 JavaScript 代码段

了解如何在 WebAssembly 库中嵌入 JavaScript 代码,以便与外界通信。

Ingvar Stepanyan
Ingvar Stepanyan

在 WebAssembly 与 Web 集成时,您需要一种调用外部 API(例如 Web API 和第三方库)的方法。然后,您需要一种存储这些 API 返回的值和对象实例的方法,以及一种稍后将这些存储的值传递给其他 API 的方法。对于异步 API,您可能还需要使用 Asyncify 在同步 C/C++ 代码中等待 promise,并在操作完成后读取结果。

Emscripten 提供了多种用于此类互动的工具:

  • emscripten::val,用于在 C++ 中存储和操作 JavaScript 值。
  • EM_JS,用于嵌入 JavaScript 代码段并将其绑定为 C/C++ 函数。
  • EM_ASYNC_JSEM_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 执行的每项操作都需要执行以下步骤:

  1. 将作为参数传递的 C++ 值转换为某种中间格式。
  2. 前往 JavaScript,读取参数并将其转换为 JavaScript 值。
  3. 执行函数
  4. 将结果从 JavaScript 转换为中间格式。
  5. 将转换后的结果返回给 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);
});

最后,对于更复杂的任意值类型,您可以针对前面提到的 val 类使用 JavaScript API。借助它,您可以将 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 familyEM_ASMEM_ASM_INTEM_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);

在两端声明后,JavaScript 库可以通过 --js-library option 与主代码相关联,将原型与相应的 JavaScript 实现相关联。

不过,这种模块格式是非标准的,需要仔细的依赖项注解。因此,它通常仅适用于高级场景。

总结

在本文中,我们介绍了在使用 WebAssembly 时将 JavaScript 代码集成到 C++ 中的各种方法。

通过添加此类代码段,您可以更清晰、更高效地表达长序列的操作,并利用第三方库、新的 JavaScript API,甚至还可以利用尚无法通过 C++ 或 Embind 表达的 JavaScript 语法功能。