WebIDL Binder
提供一种简单、轻量级的方法来绑定 C++ 代码。
WebIDL Binder
使用 WebIDL
定义了一种 接口语言 来把 C++ 和 JavaScript 粘合在一起。
该绑定器支持可以用 WebIDL
表达的 c++ 类型的子集。这个子集对于大多数情况来说已经足够了。
接下来,通过一个简单的例子来看一下绑定的流程,使用 WebIDL Binder
进行绑定的过程分为三个阶段:
- 创建一个
WebIDL
文件,用来描述 C++ 接口; - 使用绑定器生成 C++ 和 JavaScript 的胶水代码;
- 使用
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
struct
与class
一样,也是使用interface
关键字
第二步:生成胶水代码
执行命令 webidl_binder my_classes.idl glue
,会在同级目录产生 glue.cpp 和 glue.js 两个胶水代码文件
第三步:编译项目
在项目编译过程中使用胶水代码文件(glue.cpp
和 glue.js
)
-
在
emcc
命令中添加--post-js glue.js
。post-js
选项将胶水代码(glue.js)添加到编译后的 js 输出文件的末尾; -
创建一个包装器文件,比如:
my_glue_wrapper.cpp
,用来#include
要绑定的类的头文件和glue.cpp
,比如:#include "my_classes.h" #include <cstddef> // size_t #include "glue.cpp"
-
将
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.js
和output.wasm
添加到网站中,然后通过以下代码创建Foo
和Bar
对象,并调用他们的方法: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
- 总是通过
-
模块化输出
使用 ‘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 |
参考: