WebIDL Binder 提供一种简单、轻量级的方法来绑定 C++ 代码。

WebIDL Binder 使用 WebIDL 定义了一种 接口语言 来把 C++ 和 JavaScript 粘合在一起。

该绑定器支持可以用 WebIDL 表达的 c++ 类型的子集。这个子集对于大多数情况来说已经足够了。

接下来,通过一个简单的例子来看一下绑定的流程,使用 WebIDL Binder 进行绑定的过程分为三个阶段:

  1. 创建一个 WebIDL 文件,用来描述 C++ 接口;
  2. 使用绑定器生成 C++ 和 JavaScript 的胶水代码;
  3. 使用 EMScripten 编译此胶水代码;

第一步:创建 WebIDL 接口文件

创建一个描述将要绑定的 C++ 类型的 WebIDL 接口文件。该文件将复制 C++ 头文件中的一些信息。比如,我们想绑定下面的 C++ 类(my_classes.h):

class Foo
{
public:
    int getVal();
    void setVal(int v);

private:
    int m_val{0};
};

class Bar
{
public:
    Bar(long val);
    ~Bar();
    void doSomething();

private:
    int m_val;
};

IDL 接口文件就可以写成下面的形式(my_classes.idl):

interface Foo {
  void Foo();
  long getVal();
  void setVal(long v);
};

interface Bar {
  void Bar(long val);
  void doSomething();
};

从 IDL 接口文件到 C++ 代码的映射要注意:

  • IDL类定义包括了一个与接口同名的且返回值是 void 的函数。这个构造函数允许我们从 JavaScript 中创建对象,并且必须在 IDL 中定义,即使 C++ 中使用的是默认构造函数;
  • WebIDL 中的类型名与 C++ 中的不一样,比如 int 被映射成了 long,具体的类型对应关系参考 WebIDL types
  • structclass 一样,也是使用 interface 关键字

第二步:生成胶水代码

执行命令 webidl_binder my_classes.idl glue,会在同级目录产生 glue.cppglue.js 两个胶水代码文件

第三步:编译项目

在项目编译过程中使用胶水代码文件(glue.cppglue.js

  1. emcc 命令中添加 --post-js glue.jspost-js 选项将胶水代码(glue.js)添加到编译后的 js 输出文件的末尾;

  2. 创建一个包装器文件,比如:my_glue_wrapper.cpp,用来 #include 要绑定的类的头文件和 glue.cpp,比如:

    #include "my_classes.h"
    #include <cstddef>      // size_t
    #include "glue.cpp"
    
  3. my_glue_wrapper.cpp 一起添加到 emcc 编译命令中,最后的 emcc 命令包含 C++ 和 JavaScript 的胶水代码,它们是为了协同工作而构建的

    emcc my_classes.cpp my_glue_wrapper.cpp --post-js glue.js -o output.js

    现在 output.js 文件中就包含通过 JavaScript 使用 C++ 类所需的所有内容了。

    使用方式:

    一旦编译完成,C++ 对象就可以像普通的 JavaScript 对象一样在 JavaScript 中创建和使用了。将 output.jsoutput.wasm 添加到网站中,然后通过以下代码创建 FooBar 对象,并调用他们的方法:

    var f = new Module.Foo();
    f.setVal(200);
    alert(f.getVal());
    
    var b = new Module.Bar(123);
    b.doSomething();
    

    注意:

    • 总是通过 Module 对象访问对象;
    • 虽然在默认情况下,全局命名空间中的对象也是可用的,但在某些情况下它们是不可用的(例如,如果使用闭包编译器来最小化代码,或将编译后的代码包装在函数中,以避免污染全局命名空间)。当然,也可以通过将模块赋值给一个新变量:var MyModuleName = module;
    • 只能在 js 调用 WASM 代码是安全的情况下(所需文件都加载完成后),使用上面的代码;

    当没有更多的引用时,JavaScript 将自动对任何包装好的 C++ 对象进行垃圾回收。如果 C++ 对象不需要特定的清理(即它没有析构函数),那么不需要采取其他操作。

    如果一个 C++ 对象确实需要清理,就必须显式地调用 Module.destroy(obj) 来调用它的析构函数,然后删除对该对象的所有引用,以便它可以被垃圾收集。例如,如果 Bar 需要清理分配的内存:

    var b = new Module.Bar(123);
    b.doSomething();
    Module.destroy(b); // If the C++ object requires clean up
    
  4. 模块化输出

    使用 ‘WebIDL Binder’ 时,通常是创建一个库。在这种情况下,就要使用 MODULARIZE 选项。它将整个 JavaScript 输出包装在一个函数中,并返回一个解析为已初始化的 Module 实例的 Promise

    emcc my_classes.cpp my_glue_wrapper.cpp -s MODULARIZE --post-js glue.js -o output.js

    使用方式:

    var instance;
    Module().then(module => {
        instance = module;
        var f = new module.Foo();
        f.setVal(200);
        alert(f.getVal());
    
        var b = new module.Bar(123);
        b.doSomething();
        module.destroy(b);
    });
    

    当它可以安全运行已编译的代码时,也就是说它已经被下载并实例化之后,promise 才会被解析。promise 是在调用 onRuntimeInitialized 回调函数的同时被解析的,所以在使用 MODULARIZE 时不需要使用 onRuntimeInitialized 回调。

    onRuntimeInitialized 回调:

    <script type='text/javascript'>
      var Module = {
        onRuntimeInitialized: function(){
          alert("I am ready!");
        }
      }
    </script>
    <script src = "hello.js"></script>
    

    我们还可以使用 EXPORT_NAME 选项将 Module 更改为其他内容。对于库来说,这是一种很好的实践方式,因为这样它们就不会在全局作用域中包含不必要的内容,而且在某些情况下,我们希望创建多个库。编译命令可参考下面这种:

    emcc my_classes.cpp my_glue_wrapper.cpp -s MODULARIZE -s 'EXPORT_NAME="createMyModule"' --post-js glue.js -o output.js

    我们可以这样使用:

    createMyModule(/* optional default settings */).then(function(MyModule) {
        // this is reached when everything is ready, and you can call methods on Module     
        var f = new MyModule.Foo();
        f.setVal(200);
        alert(f.getVal());
    
        var b = new MyModule.Bar(123);
        b.doSomething();
        MyModule.destroy(b);
    });
    

    注意:MODULARIZE 模式中,我们不会寻找一个全局的 Module 对象来获取默认值。默认值必须作为参数传递给工厂函数。(详情见 settings.js)

WebIDL Types

C++ IDL
bool boolean
float float
double double
char byte
char* DOMString (represents a JavaScript string)
unsigned char octet
int long
long long
unsigned short unsigned short
unsigned long unsigned long
long long long long
void void
void* any or VoidPtr

参考:

  1. WebIDL Binder