WebIDL 绑定器

WebIDL 绑定器 提供了一种简单轻便的方法来绑定 C++,以便编译后的代码可以像普通的 JavaScript 库一样从 JavaScript 中调用。

WebIDL 绑定器 使用 WebIDL 来定义绑定,这是一种专门为将 C++ 和 JavaScript 粘合在一起而设计的接口语言。这不仅是绑定的一种自然选择,而且由于它处于低级,因此相对容易优化。

绑定器支持可以在 WebIDL 中表示的 C++ 类型子集。这个子集对于大多数用例来说已经足够了——使用绑定器移植的项目的例子包括 Box2DBullet 物理引擎。

本主题展示了如何使用 IDL 绑定和使用 C++ 类、函数和其他类型。

注意

WebIDL 绑定器 的另一种选择是使用 Embind。有关更多信息,请参阅 绑定 C++ 和 JavaScript——WebIDL 绑定器和 Embind

快速示例

使用 WebIDL 绑定器 进行绑定是一个三阶段过程

  • 创建一个 WebIDL 文件来描述 C++ 接口。

  • 使用绑定器生成 C++ 和 JavaScript “粘合”代码。

  • 使用 Emscripten 项目编译此粘合代码。

定义 WebIDL 文件

第一步是创建一个 WebIDL 文件,描述你将要绑定的 C++ 类型。此文件将复制 C++ 头文件中的一些信息,以一种专门为易于解析和表示代码项而设计的格式。

例如,考虑以下 C++ 类

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

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

以下 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++ 使用默认构造函数(参见上面的 Foo)。

  • WebIDL 中的类型名称与 C++ 中的类型名称不同(例如,int 映射到上面的 long)。有关映射的更多信息,请参见 WebIDL 类型

注意

structs 的定义方式与上面的类相同——使用 interface 关键字。

生成绑定粘合代码

绑定生成器 (tools/webidl_binder.py) 以 Web IDL 文件名和输出文件名作为输入,并创建 C++ 和 JavaScript 粘合代码文件。

例如,要为 IDL 文件 my_classes.idl 创建粘合代码文件 glue.cppglue.js,可以使用以下命令

tools/webidl_binder my_classes.idl glue

编译项目(使用绑定粘合代码)

要在项目中使用粘合代码文件 (glue.cppglue.js)

  1. 在你的最终 emcc 命令中添加 --post-js glue.jspost-js 选项在编译后的输出末尾添加粘合代码。

  2. 创建一个名为 my_glue_wrapper.cpp 的文件,用于 #include 你正在绑定的类的头文件和 glue.cpp。这可能包含以下内容

#include <...> // Where "..." represents the headers for the classes we are binding.
#include <glue.cpp>

注意

绑定生成器 发出的 C++ 粘合代码不包括它绑定的类的头文件,因为这些头文件不在 Web IDL 文件中。上面的步骤使这些头文件可用于粘合代码。另一种方法是在 glue.cpp 的顶部包含头文件,但这将导致每次重新编译 IDL 文件时头文件都被覆盖。

  1. my_glue_wrapper.cpp 添加到最终的 emcc 命令中。

最终的 emcc 命令包括 C++ 和 JavaScript 粘合代码,这些代码是为一起工作而构建的

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

现在,输出包含使用 JavaScript 通过 C++ 类所需的一切。

模块化输出

使用 WebIDL 绑定器时,通常你是在创建一个库。在这种情况下,使用 MODULARIZE 选项是有意义的。它将整个 JavaScript 输出包装在一个函数中,并返回一个 Promise,该 Promise 解析为初始化的 Module 实例。

var instance;
Module().then(module => {
  instance = module;
});

当可以安全地运行编译后的代码时,即下载并实例化后,promise 将解析。promise 在调用 onRuntimeInitialized 回调时解析,因此在使用 MODULARIZE 时无需使用 onRuntimeInitialized

可以使用 EXPORT_NAME 选项将 Module 更改为其他名称。这对于库来说是良好的做法,因为这样它们就不会在全局范围内包含不必要的东西,在某些情况下,你可能希望创建多个库。

在 JavaScript 中使用 C++ 类

绑定完成后,可以像使用普通的 JavaScript 对象一样在 JavaScript 中创建和使用 C++ 对象。例如,继续上面的例子,可以创建 FooBar 对象,并在其上调用方法。

var f = new Module.Foo();
f.setVal(200);
alert(f.getVal());

var b = new Module.Bar(123);
b.doSomething();

重要

始终通过 Module 对象 对象访问对象,如上所示。

虽然这些对象默认情况下也存在于全局命名空间中,但有些情况下它们将不存在(例如,如果使用 Closure Compiler 来缩小代码或将编译后的代码包装在一个函数中以避免污染全局命名空间)。当然,你可以使用任何你喜欢的名称来表示模块,将其分配给一个新的变量:var MyModuleName = Module;

重要

只有在 可以安全地调用编译后的代码 时,才能使用此代码,有关更多详细信息,请参阅该 FAQ 条目。

当不再有引用时,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

注意

当在 JavaScript 中创建 C++ 对象时,C++ 构造函数将被透明地调用。但是,无法判断 JavaScript 对象是否将要被垃圾回收,因此绑定器粘合代码无法自动调用析构函数。

通常需要销毁你创建的对象,但这取决于正在移植的库。

属性

对象属性在 IDL 中使用 attribute 关键字定义。这些属性可以在 JavaScript 中使用 get_foo()/set_foo() 访问器方法或直接作为对象的属性进行访问。

// C++
int attr;
// WebIDL
attribute long attr;
// JavaScript
var f = new Module.Foo();
f.attr = 7;
// Equivalent to:
f.set_attr(7);

console.log(f.attr);
console.log(f.get_attr());

对于只读属性,请参见 Const

指针、引用、值类型(Ref 和 Value)

C++ 参数和返回值可以是指针、引用或值类型(在堆栈上分配)。IDL 文件使用不同的装饰来表示这几种情况。

IDL 中自定义类型未装饰的参数和返回值假定为 C++ 中的指针

// C++
MyClass* process(MyClass* input);
// WebIDL
MyClass process(MyClass input);

这种假设对于 void、int、bool、DOMString 等基本类型不适用。

引用应该使用 [Ref] 进行装饰

// C++
MyClass& process(MyClass& input);
// WebIDL
[Ref] MyClass process([Ref] MyClass input);

注意

如果在引用上省略了 [Ref],生成的胶水 C++ 代码将无法编译(它在尝试将引用(它认为是指针)转换为对象时会失败)。

如果 C++ 返回一个对象(而不是引用或指针),那么返回类型应该使用 [Value] 装饰。这将分配该类的静态(单例)实例并返回它。您应该立即使用它,并在使用后删除对它的任何引用。

// C++
MyClass process(MyClass& input);
// WebIDL
[Value] MyClass process([Ref] MyClass input);

常量

使用 const 的 C++ 参数或返回类型可以在 IDL 中使用 [Const] 指定。

例如,以下代码片段展示了返回常量指针对象的函数的 C++ 和 IDL。

//C++
const myObject* getAsConst();
// WebIDL
[Const] myObject getAsConst();

对应于 const 数据成员的属性必须使用 readonly 关键字指定,而不是使用 [Const]。例如

//C++
const int numericalConstant;
// WebIDL
readonly attribute long numericalConstant;

这将在绑定中生成一个 get_numericalConstant() 方法,但不会生成相应的 setter。该属性在 JavaScript 中也将被定义为只读,这意味着尝试设置它将不会影响该值,并且在严格模式下会抛出错误。

提示

返回类型可以具有多个说明符。例如,返回常量引用的方法将在 IDL 中使用 [Ref, Const] 标记。

不可删除类(NoDelete)

如果类不能被删除(因为析构函数是私有的),请在 IDL 文件中指定 [NoDelete]

[NoDelete]
interface Foo {
...
};

定义内部类和命名空间中的类(Prefix)

在命名空间(或其他类)中声明的 C++ 类必须使用 IDL 文件 Prefix 关键字来指定范围。然后,每当在 C++ 胶水代码中引用该类时,就会使用该前缀。

例如,以下 IDL 定义确保 Inner 类被引用为 MyNameSpace::Inner

[Prefix="MyNameSpace::"]
interface Inner {
..
};

运算符

您可以使用 [Operator=] 绑定到 C++ 运算符。

[Operator="+="] TYPE1 add(TYPE2 x);

注意

  • 运算符名称可以是任何东西(add 只是一个例子)。

  • 目前支持以下二元运算符:+-*/%^&|=<>+=-=*=/=%=^=&=|=<<>>>>=<<===!=<=>=<=>&&||,以及数组索引运算符 []

枚举

枚举在 C++ 和 IDL 中的声明非常相似。

// C++
enum AnEnum {
  enum_value1,
  enum_value2
};

// WebIDL
enum AnEnum {
  "enum_value1",
  "enum_value2"
};

对于在命名空间中声明的枚举,语法稍微复杂一些。

// C++
namespace EnumNamespace {
  enum EnumInNamespace {
  e_namespace_val = 78
  };
};

// WebIDL
enum EnumNamespace_EnumInNamespace {
  "EnumNamespace::e_namespace_val"
};

当枚举在类中定义时,枚举和类接口的 IDL 定义是分开的。

// C++
class EnumClass {
 public:
  enum EnumWithinClass {
  e_val = 34
  };
  EnumWithinClass GetEnum() { return e_val; }

  EnumNamespace::EnumInNamespace GetEnumFromNameSpace() { return EnumNamespace::e_namespace_val; }
};



// WebIDL
enum EnumClass_EnumWithinClass {
  "EnumClass::e_val"
};

interface EnumClass {
  void EnumClass();

  EnumClass_EnumWithinClass GetEnum();

  EnumNamespace_EnumInNamespace GetEnumFromNameSpace();
};

在 JavaScript 中子类化 C++ 基类(JSImplementation)

WebIDL 绑定器 允许在 JavaScript 中子类化 C++ 基类。在下面的 IDL 片段中,JSImplementation="Base" 表示关联的接口(ImplJS)将是 C++ 类 Base 的 JavaScript 实现。

[JSImplementation="Base"]
interface ImplJS {
  void ImplJS();
  void virtualFunc();
  void virtualFunc2();
};

运行绑定生成器并编译后,您可以在 JavaScript 中实现该接口,如下所示。

var c = new ImplJS();
c.virtualFunc = function() { .. };

当 C++ 代码具有指向 Base 实例的指针并调用 virtualFunc() 时,该调用将到达上面定义的 JavaScript 代码。

注意

  • 必须实现您在 JSImplementation 类(ImplJS)的 IDL 中提到的所有方法,否则编译将失败并出现错误。

  • 您还需要在 IDL 文件中提供 Base 类的接口定义。

指针和比较

所有绑定函数都期望接收包装对象(包含原始指针),而不是原始指针。您通常不需要处理原始指针(这些只是内存地址/整数)。如果确实需要,编译代码中的以下函数可能会有用。

  • wrapPointer(ptr, Class) — 给定原始指针(整数),返回包装对象。

    注意

    如果您不传递 Class,它将被假定为根类——这可能不是您想要的!

  • getPointer(object) — 返回原始指针。

  • castObject(object, Class) — 返回同一个指针的包装,但指向另一个类。

  • compare(object1, object2) — 比较两个对象的指针。

注意

对于指向特定类的特定指针,始终存在一个包装对象。这允许您在该对象上添加数据,并使用标准 JavaScript 语法(object.attribute = someData 等)在其他地方使用它。

应使用 compare() 来代替直接指针比较,因为如果一个类是另一个类的子类,则可能存在具有相同指针的不同包装对象。

NULL

所有返回指针、引用或对象的绑定函数都将返回包装指针。原因是通过始终返回包装器,您可以获取输出并将其传递给另一个绑定函数,而无需该函数检查参数的类型。

一个可能令人困惑的情况是返回 NULL 指针。使用绑定时,返回的指针将是 NULL(一个全局单例,其包装指针为 0),而不是 null(JavaScript 内置对象)或 0。

void*

通过 VoidPtr 类型支持 void* 类型,您可以在 IDL 文件中使用它。您也可以使用 any 类型。

它们之间的区别在于 VoidPtr 的行为类似于指针类型,您可以获得包装对象,而 any 的行为类似于 32 位整数(这是 Emscripten 编译代码中的原始指针)。

WebIDL 类型

WebIDL 中的类型名称与 C++ 中的类型名称不完全相同。本节展示了您会遇到的更常见类型的映射。

C++

IDL

bool

boolean

float

float

double

double

char

byte

char*

DOMString(表示 JavaScript 字符串)

unsigned char

octet

int

long

long

long

unsigned short

unsigned short

unsigned long

unsigned long

long long

long long

void

void

void*

anyVoidPtr(参见 void*

注意

WebIDL 类型在 此 W3C 规范 中有完整记录。

测试和示例代码

有关完整的可运行示例,请参见 test_webidl,位于 测试套件 中。测试套件代码保证可以工作,并且涵盖了比本文更多的案例。

另一个很好的例子是 ammo.js,它使用 WebIDL 绑定器Bullet 物理引擎 移植到 Web。