它可以将 JS 绑定到你的 wasm!
在我上一篇 wasm 文章中,我介绍了如何将 C 库编译为 wasm,以便你可以在 web 上使用它。让我(和许多读者)印象深刻的一件事是,你必须手动声明要使用的 wasm 模块的函数,这种方式既粗糙又有点笨拙。为了唤醒你的记忆,这是我正在谈论的代码片段
const api = {
version: Module.cwrap('version', 'number', []),
create_buffer: Module.cwrap('create_buffer', 'number', ['number', 'number']),
destroy_buffer: Module.cwrap('destroy_buffer', '', ['number']),
};
在这里,我们声明了用 EMSCRIPTEN_KEEPALIVE
标记的函数的名称、它们的返回类型以及它们的参数类型。之后,我们可以使用 api
对象上的方法来调用这些函数。但是,以这种方式使用 wasm 不支持字符串,并且需要你手动移动内存块,这使得许多库 API 非常难用。有没有更好的方法?当然有,否则这篇文章要写什么呢?
C++ 名称修饰
虽然开发者体验足以成为构建一个工具来帮助进行这些绑定的理由,但实际上还有一个更紧迫的理由:当你编译 C 或 C++ 代码时,每个文件都是单独编译的。然后,链接器负责将所有这些所谓的对象文件混合在一起,并将它们变成一个 wasm 文件。对于 C,函数的名称仍然在对象文件中可用,供链接器使用。要调用 C 函数,你只需要名称,我们将其作为字符串提供给 cwrap()
。
另一方面,C++ 支持函数重载,这意味着只要签名不同(例如,参数类型不同),你就可以多次实现同一个函数。在编译器级别,像 add
这样的好名称会被修饰 成一些东西,这些东西将签名编码到链接器的函数名称中。因此,我们将无法再使用其名称查找我们的函数。
进入 embind
embind 是 Emscripten 工具链的一部分,它为你提供了一堆 C++ 宏,允许你注释 C++ 代码。你可以声明你计划从 JavaScript 中使用的函数、枚举、类或值类型。让我们从一些简单的函数开始
#include <emscripten/bind.h>
using namespace emscripten;
double add(double a, double b) {
return a + b;
}
std::string exclaim(std::string message) {
return message + "!";
}
EMSCRIPTEN_BINDINGS(my_module) {
function("add", &add);
function("exclaim", &exclaim);
}
与我之前的文章相比,我们不再包含 emscripten.h
,因为我们不必再用 EMSCRIPTEN_KEEPALIVE
注释我们的函数。相反,我们有一个 EMSCRIPTEN_BINDINGS
部分,我们在其中列出我们想要向 JavaScript 公开我们的函数的名称。
要编译此文件,我们可以使用与前一篇文章中相同的设置(或者,如果你愿意,可以使用相同的 Docker 镜像)。要使用 embind,我们添加 --bind
标志
$ emcc --bind -O3 add.cpp
现在剩下的就是创建一个 HTML 文件来加载我们新创建的 wasm 模块
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
console.log(Module.add(1, 2.3));
console.log(Module.exclaim("hello world"));
};
</script>
正如你所看到的,我们不再使用 cwrap()
了。这开箱即用。但更重要的是,我们不必担心手动复制内存块来使字符串工作!embind 免费为你提供这些,以及类型检查

这非常好,因为我们可以及早发现一些错误,而无需处理偶尔非常笨拙的 wasm 错误。
对象
许多 JavaScript 构造函数和函数都使用选项对象。这是 JavaScript 中的一个很好的模式,但在 wasm 中手动实现非常繁琐。embind 在这里也可以提供帮助!
例如,我想出了这个非常 有用的 C++ 函数来处理我的字符串,并且我迫切希望在 web 上使用它。这是我实现的方法
#include <emscripten/bind.h>
#include <algorithm>
using namespace emscripten;
struct ProcessMessageOpts {
bool reverse;
bool exclaim;
int repeat;
};
std::string processMessage(std::string message, ProcessMessageOpts opts) {
std::string copy = std::string(message);
if(opts.reverse) {
std::reverse(copy.begin(), copy.end());
}
if(opts.exclaim) {
copy += "!";
}
std::string acc = std::string("");
for(int i = 0; i < opts.repeat; i++) {
acc += copy;
}
return acc;
}
EMSCRIPTEN_BINDINGS(my_module) {
value_object<ProcessMessageOpts>("ProcessMessageOpts")
.field("reverse", &ProcessMessageOpts::reverse)
.field("exclaim", &ProcessMessageOpts::exclaim)
.field("repeat", &ProcessMessageOpts::repeat);
function("processMessage", &processMessage);
}
我正在为我的 processMessage()
函数的选项定义一个结构体。在 EMSCRIPTEN_BINDINGS
块中,我可以使用 value_object
来使 JavaScript 将此 C++ 值视为对象。如果我更喜欢将此 C++ 值用作数组,我也可以使用 value_array
。我还绑定了 processMessage()
函数,剩下的就是 embind 的魔力 了。我现在可以从 JavaScript 调用 processMessage()
函数,而无需任何样板代码
console.log(Module.processMessage(
"hello world",
{
reverse: false,
exclaim: true,
repeat: 3
}
)); // Prints "hello world!hello world!hello world!"
类
为了完整起见,我还应该向你展示 embind 如何允许你公开整个类,这与 ES6 类带来了很多协同作用。你现在可能已经开始看到一种模式了
#include <emscripten/bind.h>
#include <algorithm>
using namespace emscripten;
class Counter {
public:
int counter;
Counter(int init) :
counter(init) {
}
void increase() {
counter++;
}
int squareCounter() {
return counter * counter;
}
};
EMSCRIPTEN_BINDINGS(my_module) {
class_<Counter>("Counter")
.constructor<int>()
.function("increase", &Counter::increase)
.function("squareCounter", &Counter::squareCounter)
.property("counter", &Counter::counter);
}
在 JavaScript 方面,这几乎感觉像一个原生类
<script src="/a.out.js"></script>
<script>
Module.onRuntimeInitialized = _ => {
const c = new Module.Counter(22);
console.log(c.counter); // prints 22
c.increase();
console.log(c.counter); // prints 23
console.log(c.squareCounter()); // prints 529
};
</script>
C 呢?
embind 是为 C++ 编写的,只能在 C++ 文件中使用,但这并不意味着你不能链接到 C 文件!要混合 C 和 C++,你只需要将输入文件分成两组:一组用于 C 文件,另一组用于 C++ 文件,并按如下方式扩充 emcc
的 CLI 标志
$ emcc --bind -O3 --std=c++11 a_c_file.c another_c_file.c -x c++ your_cpp_file.cpp
结论
在使用 wasm 和 C/C++ 时,embind 在开发者体验方面为你带来了巨大的改进。本文并未涵盖 embind 提供的所有选项。如果你有兴趣,我建议继续阅读 embind 的文档。请记住,使用 embind 可能会使你的 wasm 模块和 JavaScript 粘合代码的大小增加多达 11k(gzip 压缩后)——尤其是在小型模块上。如果你只有一个非常小的 wasm 表面,那么在生产环境中,embind 的成本可能高于其价值!尽管如此,你绝对应该尝试一下。