Emscripten's embind

它将 JS 绑定到您的 wasm!

在我上一篇文章中,我提到了 了解如何将 C 库编译为 wasm 库,以便在网页上使用。一件事 让我(和许多读者)感到很不舒服的 您必须手动声明您所使用的 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++ 代码添加注释。您可以声明哪些函数、枚举 类或值类型。我们开始吧 只是一些普通函数:

#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 构造函数和函数都使用 options 对象。这是一个不错的选择 模式,但在 wasm 中手动实现极其繁琐。Embind 也可以在这里提供帮助!

例如,我想出这个非常有用的 C++ 函数,用于处理我的数据 字符串,我迫切想在网页上使用它。具体操作步骤如下:

#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++值视为对象我还可以使用value_array 将此 C++ 值用作数组。我还绑定了 processMessage() 函数, 其余部分则体现了魔法。现在,我可以从以下代码中调用 processMessage() 函数: 没有任何样板代码的 JavaScript:

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

总结

embind 可显著提升 使用 wasm 和 C/C++。本文并未涵盖绑定优惠的所有选项。 如果您感兴趣,建议您继续学习 embind 的 文档。 请注意,使用 embind 可能会使您的 wasm 模块和 在进行 gzip 处理时,JavaScript 粘合代码的大小高达 11k,最值得注意的是 模块。如果您只有非常小的 Wasm 表面,则 embind 的费用可能超过 值得在生产环境中使用!但无论如何,您绝对应该提供 。