Emscripten WebIDL Binder 实战指南:用 WebIDL 把 C++ 类绑定给 JavaScript 调用
【免费下载链接】emscriptenEmscripten: An LLVM-to-WebAssembly Compiler项目地址: https://gitcode.com/gh_mirrors/em/emscripten
WebIDL Binder 是 Emscripten 提供的一种轻量级 C++ 绑定方案:先用 WebIDL(一种专门为打通 C++ 与 JavaScript 而设计的接口描述语言)描述要暴露的 C++ 类型,再由绑定生成器产出 C++ 与 JavaScript 两套"胶水"代码,最终让编译产物中的 C++ 类可以像普通 JavaScript 库一样被直接new、调用方法、读写属性。读完本文,你将掌握编写 IDL 文件、生成与编译胶水代码、处理指针/引用/值语义、绑定运算符与枚举、在 JavaScript 中继承 C++ 基类等一系列完整实战技能。
为什么需要 WebIDL Binder
把 C++ 代码编译到 WebAssembly 后,JavaScript 无法直接调用其中的类与对象——只能通过导出的函数和原始内存地址交互。WebIDL Binder 的作用就是在两者之间架起一座桥:以 WebIDL 文件为"契约"描述 C++ 接口,然后自动生成双向转换代码,使 JavaScript 一侧可以像使用原生 JS 对象一样使用 C++ 对象。
它的核心优势在于:
- 接口描述语言天然契合:WebIDL 本就是 W3C 为胶合 C++ 与 JavaScript 而设计的低级接口语言,作为绑定描述既自然又便于优化;
- 轻量直接:绑定的是 C++ 类型的子集,这些子集覆盖了绝大多数使用场景;
- 有成熟实战验证:Box2D、Bullet 物理引擎(ammo.js)等知名项目都是通过该 Binder 移植到 Web 的。
Emscripten 官方同时提供另一套功能更丰富的绑定方案 Embind,当 WebIDL Binder 的表达能力不足以满足需求时,可以转向 Embind 对比选用。
三步绑定流程概览
使用 WebIDL Binder 绑定 C++ 类是一个固定三步骤过程:
- 编写 WebIDL 文件:描述要绑定的 C++ 接口(类、函数、属性、枚举等);
- 生成胶水代码:用绑定生成器(tools/webidl_binder.py)根据 IDL 文件产出 C++ 胶水文件(如
glue.cpp)与 JavaScript 胶水文件(如glue.js); - 编译工程:把 C++ 胶水文件与业务 C++ 代码一起交给 emcc 编译,并把 JavaScript 胶水文件通过
--post-js追加到输出中。
下面逐一展开。
定义 WebIDL 文件
第一步是创建描述待绑定 C++ 类型的 WebIDL 文件。它会在一定程度上重复 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、与 interface 同名的"构造器"方法(如上例Foo中的void Foo();),即使 C++ 使用的是默认构造器也必须在 IDL 中显式声明——它负责在 JavaScript 中创建对象的能力; - 类型名映射:WebIDL 的类型名与 C++ 并不一一对应,例如上例 C++ 的
int在 IDL 中写作long。完整的类型映射表见下文"WebIDL 类型映射"小节。
另外,C++ 的struct与类一样,同样使用interface关键字在 IDL 中声明(struct与class在 C++ 中本就只是默认访问权限不同)。
在仓库的 test/webidl/test.idl 中可以看到一个覆盖极广的完整 IDL 实例,其中用到了interface、attribute、readonly attribute、enum、implements(继承)、static方法、optional参数、数组类型以及[Prefix]、[Const]、[Ref]、[Value]、[Operator]、[JSImplementation]、[NoDelete]、[BindTo]、[BoundsChecked]等大量扩展属性,是学习 IDL 写法的绝佳模板。
生成绑定胶水代码
绑定生成器(tools/webidl_binder.py)接收一个 WebIDL 文件名和一个输出文件名,产出对应的 C++ 与 JavaScript 胶水代码文件。
例如,为 IDL 文件my_classes.idl生成glue.cpp与glue.js:
tools/webidl_binder my_classes.idl glue生成器的完整调用形式(见 tools/webidl_binder.py 顶部的 argparse 定义)为:
tools/webidl_binder [--wasm64] infile outfileinfile:WebIDL 输入文件路径;outfile:输出文件基名,生成器会据此创建<outfile>.cpp与<outfile>.js;--wasm64:为 wasm64 目标生成绑定代码。
生成器还会读取环境变量IDL_CHECKS来控制胶水方法中的参数类型检查强度(见 tools/webidl_binder.py 头部注释):
IDL_CHECKS=FAST:跳过大部分参数类型检查,比默认模式快约 3 倍;IDL_CHECKS=ALL:进行全面的参数类型检查(非法数字、非法指针、非法字符串等),比默认模式慢约 5 倍;- 其他任何取值(包括默认不设置):进入 legacy 兼容模式。
官方测试 test/test_core.py 的test_webidl(L7910-L7954)正是通过env_modify({'IDL_CHECKS': mode})分别以DEFAULT、ALL、FAST三种模式驱动生成器,验证三种检查强度下的行为,其中ALL模式还叠加了-sASSERTIONS编译选项。
编译工程(使用胶水代码)
要在工程中使用生成的glue.cpp与glue.js:
追加 JS 胶水:在最终emcc命令中加入
--post-js glue.js。--post-js选项会把胶水代码追加到编译输出的末尾(该选项的完整说明见 emcc 文档)。编写 C++ 包装文件:创建类似
my_glue_wrapper.cpp的文件,在其中#include被绑定类的头文件以及glue.cpp:#include <...> // 此处为被绑定类的头文件 #include <glue.cpp>说明:生成器产出的 C++ 胶水代码并不包含被绑定类的头文件——因为这些头文件不在 WebIDL 文件中。上面的包装文件正是把这些头文件提供给胶水代码的途径。另一种做法是把头文件直接
#include到glue.cpp顶部,但这样每次重新编译 IDL 文件时头文件包含都会被覆盖,所以不推荐。仓库测试中的做法可见 test/webidl/test.cpp:先
#include "test.h"(被绑定类头文件),再#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输出产物即包含在 JavaScript 中使用 C++ 类所需的全部内容。
值得一提的是,生成器在 C++ 胶水代码中通过EMSCRIPTEN_KEEPALIVE强制导出了两个自带的分配器webidl_malloc/webidl_free(见 tools/webidl_binder.py 中extern "C"段落),用于 JS 侧大字符串/大数组的临时 C 存储(ensureCache),因此使用者无需额外把malloc/free加入-sEXPORTED_FUNCTIONS。若业务代码仍需直接使用malloc/free,可仿照测试在编译命令中加入-sEXPORTED_FUNCTIONS=_malloc,_free。
模块化输出(MODULARIZE 与 EXPORT_NAME)
用 WebIDL Binder 时,通常你正在做的是一个"库"。此时非常适合启用MODULARIZE选项:它会把整个 JavaScript 输出包裹进一个函数,并返回一个 Promise,该 Promise 解析为初始化完成的 Module 实例:
var instance; Module().then(module => { instance = module; });Promise 在可以安全运行编译代码时被解析——即模块下载并实例化完成之后。它被解析的时机与onRuntimeInitialized回调触发时机相同,因此使用MODULARIZE时就不需要再使用onRuntimeInitialized。
还可以用EXPORT_NAME选项把Module改成其他名字。对于库来说这是良好实践:既避免在全局作用域中塞入无关内容,也能在需要时创建多个互不冲突的实例。
在 JavaScript 中使用 C++ 类
绑定完成后,C++ 对象在 JavaScript 中就可以像普通 JS 对象一样创建和使用。接续上面的例子:
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 中关于 safe-to-call compiled functions 的说明,即模块初始化完成之后)。
垃圾回收与手动销毁
JavaScript 会在没有任何引用时自动垃圾回收被包装的 C++ 对象。如果 C++ 对象不需要特殊的清理(即没有析构函数),则无需任何额外操作。
如果 C++ 对象确实需要清理,必须显式调用Module.destroy(obj)来触发其析构函数,然后丢弃对该对象的所有引用以便其被垃圾回收。例如Bar若分配了需要清理的内存:
var b = new Module.Bar(123); b.doSomething(); Module.destroy(b); // 如果 C++ 对象需要清理注意:在 JavaScript 中创建 C++ 对象时,其构造器是被透明调用的。但 JavaScript 无法预知对象即将被垃圾回收,因此胶水代码无法自动调用析构函数。你通常需要自己销毁所创建的对象,具体取决于被移植库的要求。
从源码层面看(tools/webidl_binder.py 中的destroy实现),destroy会调用包装对象的__destroy__方法,并将其从缓存中移除,以便对象能被 GC 且其上附加的引用被释放。相应地,只有没有[NoDelete]标记的接口才会生成__destroy__方法。
属性(Attributes)
对象属性在 IDL 中用attribute关键字定义。在 JavaScript 中既可以通过get_foo()/set_foo()访问器方法访问,也可以直接作为对象属性读写:
// C++ int attr;// WebIDL attribute long attr;// JavaScript var f = new Module.Foo(); f.attr = 7; // 等价于: f.set_attr(7); console.log(f.attr); console.log(f.get_attr());只读属性(const数据成员)的写法见下文"Const"小节。
数组属性在 IDL 中写作attribute long[] arr;形式。结合源码(tools/webidl_binder.py 属性处理分支)可以看到:数组属性生成的 getter 签名是(index),setter 签名是(index, value),即在 JavaScript 中以get_int_array(0)、set_int_array(0, 42)的方式按索引访问。测试 test/webidl/post.js 中大量使用了这一形式(包括get_struct_array(0).get_attr1()这种返回结构体包装对象再取属性的链式访问)。若属性标记了[BoundsChecked],setter 还会执行越界检查(array_bounds_check),越界访问将抛出异常。
指针、引用与值类型(Ref 和 Value)
C++ 的参数和返回类型可以是指针、引用或值类型(栈上分配)。IDL 文件用不同的修饰符区分这三种情况。
未修饰 = 指针
IDL 中自定义类型未加修饰的参数与返回值,默认视为 C++ 中的指针:
// C++ MyClass* process(MyClass* input);// WebIDL MyClass process(MyClass input);这一默认规则不适用于
void、int、bool、DOMString等基础类型。
[Ref] = 引用
引用类型需要用[Ref]修饰:
// C++ MyClass& process(MyClass& input);// WebIDL [Ref] MyClass process([Ref] MyClass input);注意:如果引用上漏写
[Ref],生成的胶水 C++ 将无法编译(它试图把"以为是指针"的引用转换成对象时必然失败)。源码中生成调用参数时会依据Ref属性决定是否在实参前加解引用符*(见 tools/webidl_binder.py 中call_args的拼接逻辑),漏写会导致类型不匹配。
[Value] = 按值返回对象
如果 C++ 返回的是对象(而非引用或指针),返回类型应使用[Value]修饰。这会分配一个静态(单例)的该类实例并返回它。你应当立即使用它,并在使用后丢弃对它的所有引用。
// C++ MyClass process(MyClass& input);// WebIDL [Value] MyClass process([Ref] MyClass input);使用[Value]返回的对象要求对应类有可用的零参构造器(测试 test/webidl/test.idl 中[Value] RefUser getCopy();旁注释 "must have zero-arg constructor" 即此约束)。
常量(Const)
C++ 中使用const的参数或返回类型,在 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 中也被定义为只读:尝试设置它不会影响实际值,且在严格模式下会抛出错误。测试 test/webidl/post.js 中sme.immutableAttr = 1被 try/catch 包裹、随后再次读取值仍不变,正是对该行为的验证。
技巧:返回类型可以同时带多个修饰符。例如返回常量引用的方法在 IDL 中标记为
[Ref, Const](仓库测试 test/webidl/test.idl 中的[Operator="+="] void incInPlace([Const, Ref] Inner i);就是[Const, Ref]组合使用的实例)。
不可删除的类(NoDelete)
如果某个类无法被删除(例如析构函数是私有的),就在 IDL 文件中指定[NoDelete]:
[NoDelete] interface Foo { ... };标记[NoDelete]后,绑定不会为该类生成__destroy__方法,调用Module.destroy()会抛出错误(源码destroy中if (!obj['__destroy__']) throw ...)。仓库测试 test/webidl/test.idl 中的ISmallObject、IObjectProvider即是[NoDelete]接口,对应地它们的 JS 实现JSSmallObject、JSObjectProvider也用[JSImplementation]关联,构成一套"抽象接口 + JS 实现"的完整用例。
内部类与命名空间内的类(Prefix)
声明在命名空间(或另一个类)内部的 C++ 类,必须在 IDL 文件中使用Prefix关键字指明作用域。此后,C++ 胶水代码中引用该类时都会带上该前缀。
例如,下面的 IDL 定义确保Inner类在 C++ 中被引用为MyNameSpace::Inner:
[Prefix="MyNameSpace::"] interface Inner { .. };仓库测试 test/webidl/test.idl 中[Prefix="Space::"] interface Inner配合 test/webidl/test.h 中namespace Space { struct Inner {...} }使用,而 test/webidl/post.js 里new TheModule.Inner()直接通过模块访问——即命名空间内的类在 JavaScript 侧依然以顶层类名出现。
运算符绑定(Operator)
可以用[Operator=]绑定 C++ 运算符:
[Operator="+="] TYPE1 add(TYPE2 x);注意事项:
- 运算符的方法名可以是任意名字(
add只是示例,绑定时以[Operator]指定的符号为准); - 目前支持以下二元运算符:
+、-、*、/、%、^、&、|、=、<、>、+=、-=、*=、/=、%=、^=、&=、|=、<<、>>、>>=、<<=、==、!=、<=、>=、<=>、&&、||,以及数组下标运算符[]。
仓库测试对运算符绑定有系统覆盖(test/webidl/test.idl 的Inner接口):[Operator="[]"] long getAsArray(long x)绑定下标运算、[Operator="+="] void incInPlace(...)绑定复合赋值、[Operator="+", Value] Inner add(...)绑定加法且按值返回、[Operator="*"] long mul2(long x)绑定乘法,test/webidl/post.js 中相应调用(new TheModule.Inner().getAsArray(12)、new TheModule.Inner(1).add(new TheModule.Inner(2)).get_value()等)即验证这些绑定正确工作。
枚举(enums)
枚举在 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(); };注意 IDL 枚举值字符串(如"EnumNamespace::e_namespace_val"、"EnumClass::e_val")携带完整的 C++ 限定名,这正是生成器定位真实 C++ 枚举值的关键。
从 test/webidl/post.js 可以看到枚举在 JavaScript 侧的使用方式:
- 顶层枚举值直接挂在模块上:
TheModule.enum_value1; - 类内枚举通过类访问:
TheModule.EnumClass.e_val(同时类方法GetEnum()返回对应值); - 命名空间内枚举也挂在顶层模块上:
TheModule.e_namespace_val。
在 JavaScript 中子类化 C++ 基类(JSImplementation)
WebIDL Binder 允许在 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 中列出的所有方法,否则编译会报错(源码 tools/webidl_binder.py 中会生成if (!self.hasOwnProperty('...')) throw 'a JSImplementation must implement all functions, you forgot ...'的运行时检查); - 同时还需要在 IDL 文件中提供
Base类自身的接口定义。
仓库测试 test/webidl/test.idl 中[JSImplementation="Child2"] interface Child2JS与[JSImplementation="VirtualBase"] interface ConcreteJS是完整范例:前者在 JS 中替换虚函数实现,test/webidl/post.js 中c3.virtualFunc = function() {...}后调用runVirtualFunc(c3)验证 C++ 侧虚函数分发能到达 JS 实现;后者验证抽象基类(纯虚函数)的正确处理——特别是const虚函数(constFunc())在 JS 实现中必须以[Const] void constFunc();声明,否则编译会失败。
函数重载(overloads)与 BindTo
C++ 允许函数重载——多个同名成员函数仅参数不同。默认情况下,WebIDL Binder 允许绑定仅在参数个数上不同的重载函数:
// C++ class OverloadTest { public: void test(int arg1, int arg2) { ... } void test(int arg) { ... } }; // WebIDL interface OverloadTest { void OverloadTest(); void test(long arg1, long arg2); void test(long arg); };如果重载函数在其他方面不同(例如参数类型不同),可以使用[BindTo]属性告诉工具要绑定(即调用)哪个函数名:
// C++ class BindToTest { public: void test(const char* arg) { ... } void test(int arg) { ... } }; // WebIDL interface BindToTest { void BindToTest(); [BindTo="test"] void testString([Const] DOMString arg); [BindTo="test"] void testInt(long arg); };此例中,C++ 函数test(const char*)在 JavaScript 中命名为testString,test(int)命名为testInt。
注意:
[BindTo]也可以单纯用于重命名函数,例如把MyFunctionName重命名为myFunctionName。
从源码看(tools/webidl_binder.py 中render_function的调用),重载解析的核心约束有两个:一是同一函数的所有重载必须具有相同的返回类型;二是按参数个数(而不是类型组合)区分签名,optional参数会被折叠进"更少参数"的签名集合,因此无法仅靠类型不同来区分两个长度相同的签名。测试 test/webidl/test.idl 中Child1的getValSqr(optional long more)与getValTimes(optional long times=1)(含默认参数),以及 test/webidl/post.js 中new TheModule.Child1(8)(重载构造器按参数个数自动选择)与bindTo.testString('hello')/bindTo.testInt(10)都是这些规则的直接验证。
指针与比较操作
所有绑定函数都期望接收包装对象(内部含一个原始指针)而非裸指针。正常情况下你不应该直接处理裸指针(它本质上只是内存地址/整数)。如果确实需要,编译产物中提供以下函数:
wrapPointer(ptr, Class)—— 给定裸指针(整数),返回包装对象。注意:如果不传
Class,将默认假定为根类——这很可能不是你想要的!getPointer(object)—— 返回裸指针。castObject(object, Class)—— 返回同一指针、但按另一个类包装的对象。compare(object1, object2)—— 比较两个对象的指针。
注意:对于"某个指针 + 某个类"的组合,始终只有一个包装对象。这允许你在该对象上附加数据,并在别处以普通 JavaScript 语法使用(
object.attribute = someData等)。compare()应取代直接的指针比较,因为当一个类是另一个类的子类时,可能存在拥有同一指针的不同包装对象。
这些工具函数全部实现于 tools/webidl_binder.py 的 JS 胶水公共段:wrapPointer基于每个类的__cache__缓存实现"一指针一包装"(ret = cache[ptr]; if (ret) return ret; ... cache[ptr] = ret),castObject即wrapPointer(obj.ptr, __class__),compare即obj1.ptr === obj2.ptr,另有文档未单列但同样暴露的getClass(obj)返回对象所属类。此外,模块上还暴露了Module.NULL(包装了指针 0 的全局单例,见下文)。
NULL 指针的处理
所有返回指针、引用或对象的绑定函数都会返回包装后的指针。原因在于:始终返回包装对象,就可以把输出直接传给另一个绑定函数,而无需该函数检查参数类型。
一个容易困惑的场景是返回NULL指针时:使用绑定时,返回的将是NULL(一个包装指针为 0 的全局单例),而不是 JavaScript 内置的null或数字0。这一点在源码中对应Module['NULL'] = wrapPointer(0);的定义。
void* 类型
void*类型通过VoidPtr类型在 IDL 文件中使用,也可以使用any类型。
两者的区别在于:VoidPtr表现得像指针类型——你得到的是一个包装对象;而any表现得像 32 位整数(这正是 Emscripten 编译代码中裸指针的形态)。
源码中interface VoidPtr {};被预置进解析器输入(见 tools/webidl_binder.py 中p.parse('interface VoidPtr {...}')),使 IDL 可以直接引用该类型。仓库测试 test/webidl/test.idl 中VoidPtr voidStar(VoidPtr something);与any GetVoidPointer();/void SetVoidPointer(any ptr);两种风格并存,test/webidl/post.js 中sme.voidStar(sme)(传包装对象)与voidPointerUser.SetVoidPointer(3)(传整数)分别验证了两者的行为。
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* | any或VoidPtr(见上文"void* 类型") |
说明:WebIDL 类型的完整规范见 W3C 的 WebIDL 规范文档。上述
byte/octet/unsigned short/unsigned long等映射在仓库测试 test/webidl/test.idl 的TypeTestClass接口中均有逐一验证(ReturnCharMethod/AcceptCharMethod、ReturnUnsignedCharMethod等,对应 test/webidl/test.h 中接受/返回char、unsigned char、unsigned short、unsigned long的 C++ 方法)。
另外值得补充的是,WebIDL Binder 还支持DOMString[]、byte[]、octet[]等数组形态的参数:DOMString[]会从 JS 字符串数组转换为 C 字符串数组,byte[]/octet[]则接受包含字节数据的 TypedArray 或二进制缓冲区(test/webidl/post.js 中byteArrayTest(bufferAddr)展示了把_malloc出的地址直接传给byte[]参数、domStringTest('...')展示字符串直传两种用法)。
测试与示例代码
完整的可运行示例位于仓库测试套件中:
- 测试驱动:test/test_core.py 中的
test_webidl(L7910-L7954)——先调用WEBIDL_BINDER生成胶水,再以--post-js=glue.js编译,并支持DEFAULT/ALL/FAST三种IDL_CHECKS模式与内存增长(ALLOW_MEMORY_GROWTH)组合。它同时验证了闭包压缩下必须配合MODULARIZE+EXPORT_NAME(createModule)使用、wasm64 需传--wasm64等边界情况; - 测试素材:IDL 定义见 test/webidl/test.idl,C++ 实现见 test/webidl/test.cpp(含
#include "glue.cpp"的标准用法),被绑定类头文件见 test/webidl/test.h,JavaScript 侧完整调用脚本见 test/webidl/post.js(覆盖构造器、继承、虚函数替换、属性、数组、枚举、运算符、类型检查失败路径、百万次创建/销毁压力循环、堆重分配后数组复制正确性等远超本文列举的场景)。
测试套件中的代码保证可用,且覆盖面比本文更广。另一个可参考的实战案例是 ammo.js——它正是使用 WebIDL Binder 把 Bullet 物理引擎移植到 Web 的,其使用模式(作为独立库、MODULARIZE模块化输出、通过Module.Ammo命名空间访问)与本指南描述的工作流一致。
常见问题与使用要点小结
- 构造器必须声明:IDL 中每个可创建的 interface 都要有同名
void方法,即使 C++ 侧用默认构造器; - 类型名用 IDL 词汇:
int写long、char*写DOMString、bool写boolean,数组用[]后缀; - 语义修饰符:默认指针、
[Ref]引用、[Value]按值返回、[Const]常量、[NoDelete]禁删、[Prefix]命名空间限定、[BindTo]重命名/重载区分、[JSImplementation]JS 继承、[Operator]运算符、[BoundsChecked]数组越界检查; - 编译组合:
--post-js glue.js+ 包装文件#include头文件与glue.cpp;做库时启用MODULARIZE,并用EXPORT_NAME重命名导出; - 对象生命周期:需要析构的对象必须显式
Module.destroy(obj);[NoDelete]类不可销毁; - 检查模式:通过环境变量
IDL_CHECKS(DEFAULT/ALL/FAST)在性能与参数校验强度之间取舍。
【免费下载链接】emscriptenEmscripten: An LLVM-to-WebAssembly Compiler项目地址: https://gitcode.com/gh_mirrors/em/emscripten
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考