-em** 语言模块** · 从 C++ 声明到 WebAssembly(Emscripten)导出链路的实现与配置参考
本文档描述本仓库在开源 SWIG 基础上扩展的 Emscripten Embind 目标模块实现要点,供开发、集成与评审使用。实现主体位于 Source/Modules/embind.cxx,接口与 typemap 位于 Lib/em/。
1. 背景
在 WebAssembly 路线下,把既有 C++ 库暴露给浏览器或 Node 运行时,业界常见做法是使用 Emscripten 自带的 embind :在 C++ 里用 EMSCRIPTEN_BINDINGS 配合 class_、function、enum_ 等 API,把类型与方法注册到 JavaScript 侧。
这一方式能力完整,但存在典型的工程痛点:
- 绑定代码以手写为主
每个类、重载、枚举、指针策略都需要在 C++ 中显式编写 embind 注册语句。接口一多,胶水层体积大、重复多,且与头文件中的声明没有自动对应关系。 - 接口演进时同步成本高
C++ 头文件增删改方法、改签名或重载后,必须人工回到绑定文件逐项修改;漏改往往在运行期才暴露,回归成本高。 - TypeScript 体验与文档缺口
工具链上常会配合 TypeScript 声明文件(.d.ts) 做类型提示。Emscripten / 周边工具若生成或推导.d.ts,多数情况下仅提供签名,不带 JSDoc/TSDoc 等注释;而 C++ 侧由 Doxygen 等维护的说明无法自动出现在 TS 侧,IDE 补全「看得见方法、看不见含义」。
本模块的定位 :把 SWIG 作为「从 C++ 声明到 embind 注册代码」的生成器,使绑定层随 .i 与头文件可重复生成 ;并在生成路径上把 Doxygen 注释写入 embind 的 .doc("..."),使运行时/调试侧能携带文档字符串,并为后续 在 .d.ts 中同步注释(若扩展发射)提供同源素材。这样将「手写 embind + 手动维护 d.ts」中的机械劳动收敛为一条生成流水线。
2. 范围与术语
| 术语 | 含义 |
|---|---|
-em |
SWIG 命令行选项,选中 Embind 语言模块(见 Source/Modules/swigmain.cxx 中 " -em" 注册)。 |
| 封装单元 | 生成的 *_wrap.cxx(或工程指定的输出文件),内含 EMSCRIPTEN_BINDINGS 注册代码。 |
| JS 绑定名 | 传入 embind 的字符串标识(如 .function("name", ...)),决定 JavaScript/TypeScript 侧调用名。 |
mxObject<T> |
与 smart_ptr 配合使用的托管包装类型(由项目 C++ 运行时提供;生成代码假定其存在)。 |
不在本文范围 :Emscripten 链接参数、WASM 加载方式。本文实现以 C++ 侧 embind 注册代码 为主;.d.ts** 的生成与是否带注释**取决于是否在本仓库或其它工具链中增加对应发射逻辑(当前 embind 后端已具备向 .doc() 注入文档的基础)。
3. 构建流水线
plain
C/C++ 头文件 + *.i
→ swig -c++ -em -o <module>_wrap.cxx <module>.i
→ em++/emcc 与业务源码一并编译
→ .wasm + JS 胶水,运行时通过 embind 导出符号
生成文件开头可带 UTF-8 BOM (\xEF\xBB\xBF),便于在 Windows 下被部分工具正确识别为 UTF-8(含中文注释/Doxygen 时)。
4. 生成代码结构(逻辑)
- 头文件区 :包含、前置声明(
f_header/f_begin等分段由top()初始化)。 - 枚举 :
enum_<EnumType>("sym_name").value(...)...写入f_wrapper_enum。 - 类 :
class_<T>(...)或class_<T, base<B>>(...);可选.smart_ptr<...>、.allow_subclass<SwigDirector_T>(...)。 - 成员/静态方法 :
.function/.class_function,附带select_overload、allow_raw_pointers、async、pure_virtual、.doc("...")等。
具体拼接逻辑集中在 EmBind::classHandler、printFunctionParam、printExtendFunction、director 相关路径。
5. 功能规格
5.1 JavaScript 侧重载区分与形参名(embindJsBindingName)
对非构造方法,默认 JS 绑定名为:
basename + "(" + 形参名列表 + ")"
basename通常为 C++ 方法名,可被 函数重命名(见 5.2)覆盖。- 实例方法跳过第一个
this参数;静态方法使用完整参数列表。 - 无名参数使用
arg0,arg1, ...
效果:多重重载在 JS 侧具备可读、稳定的区分键,便于与 TypeScript 重载声明对齐。
实现参考 :embindJsBindingName(),printFunctionParam() 中构造 js_binding_name。
5.2 函数重命名(feature:customem:rename)
在生成 JS 绑定字符串 时,若当前节点存在属性 feature:customem:rename,则以其值作为 embindJsBindingName 的基准名,不再使用 C++ name。
- C++ 侧仍通过
&Class::method/select_overload指向真实符号。 - 适用于:C++ 命名与脚本侧命名规范不一致、或与已有 JS API 对齐。
接口文件示例 (须与实际 SWIG feature 解析一致,建议在工程中用 swig -E 或调试确认节点属性):
plain
%feature("customem:rename", "add") MyClass::add1;
实现参考 :printFunctionParam() 内 Getattr(n, "feature:customem:rename")。
5.3 智能指针(feature:emsmart)
对 类 节点:若存在 feature:emsmart,在 class_<T>(...) 链上追加:
cpp
.smart_ptr<mxObject<T>>("T的sym:name")
其中 T 为当前包装类类型,字符串参数与类的 embind 注册名一致(sym:name)。
语义 :将 embind 的 smart_ptr 策略与项目自定义的 mxObject<T> 结合,使该类型在 JS 侧可按智能指针语义传递/持有(具体生命周期由 mxObject 与 Emscripten 侧实现定义)。
限制 (源码注释):当前「是否为智能指针类」的识别依赖显式 feature,并非 自动从 std::shared_ptr<T> 等类型推导;使用前需在接口中显式标注。
实现参考 :classHandler() 中 Getattr(n, "feature:emsmart") 分支。
接口文件示例:
plain
%feature("emsmart") MyClass;
(具体 emsmart 的赋值形式以 SWIG feature 语法为准:无值 feature 或 "1" 等,需与 Getattr 非空判断一致。)
5.4 异步导出(feature:customem:aysnc)
实现中属性名为 customem:aysnc(拼写与 async 不一致,属历史笔误;文档与接口文件需与实现保持一致)。
当该 feature 存在于 方法 节点时,在合适的 .function / .class_function 上追加 ,async()(embind 异步绑定)。
实现参考 :printFunctionParam() 中 Getattr(n, "feature:customem:aysnc")。
示例 (注释见于 Examples/em/example.h):
plain
%feature("customem:aysnc", "true") MyClass::add;
5.5 模板实例化类的自定义封装(feature:customem:config)
若 类 由 %template 实例化且带有 feature:customem:config,则 不走 默认 class_<T> 分支:改为从模板实参列表解析出参数串,并生成形如:
cpp
<config 宏或模板名><模板实参列表>("类sym名");
用于将特定模板实例映射到项目自定义的 embind 注册模板(具体由 config_node 字符串决定)。
实现参考 :classHandler() 中 Getattr(n, "feature:customem:config") 分支,SwigType_templateargs / SwigType_parmlist 解析。
5.6 Doxygen 与 embind .doc("...")
- 模块在
main()中启用scan_doxygen_comments = 1,解析注释供Getattr(n, "doxygen")使用。 embindAppendDocIfAny()在支持的方法上追加.doc("转义后的文本")。- UTF-8 通过
embindEscapeForCxxStringLiteral按字节转义写入 C++ 字符串字面量,避免 DOHPrintf路径破坏多字节字符。
与 §1 的关系 :注释写入 .doc("...") 后,可在运行时或调试工具链中随导出符号携带说明;若需弥补 官方/常见工具生成的 .d.ts 无注释 的问题,可在同一 SWIG 遍历中扩展发射 带 TSDoc/JSDoc 的声明文件,与本文 §1 中的「同源素材」一致。
5.7 属性:$getter / $setter(注释约定)
解析 Doxygen 正文中 $getter(prop)、$setter(prop),按类名与属性名配对 getter/setter 方法;当两者均登记后,生成:
cpp
.property("prop_lower", &Class::getter, &Class::setter)
属性名在生成时统一为小写。配对成功后 不再 为这两个方法生成普通 .function 绑定。
实现参考 :parsePropertyFromComment(),g_property_methods,printFunctionParam() 内分支。
5.8 继承与 Director
- 基类 :若存在基类,生成
class_<Derived, base<Base>>(...). - Director :在开启 director 且满足内部
is_dev_class等条件时,追加.allow_subclass<SwigDirector_T>(...);虚函数可经optional_override等路径导出(见lambdafunction、classDirectorInit等)。
5.9 指针与重载、可选参数
- 裸指针 :
hasPointerParam/hasStaticPointerParam为真时,为相应.function/.class_function增加allow_raw_pointers()(部分路径对char*有特殊处理逻辑,以源码为准)。 - 重载 :无默认参数时多用
select_overload<...>(&Class::method);存在默认参数时,首重载用select_overload,其余可能走optional_overridelambda 包装(isFirstOveradName等)。 - 纯虚 :
pure_virtual()。
5.10 %extend 成员
若节点带 feature:extend,通过 printExtendFunction() 生成绑定,.function 指向扩展逻辑而非简单 &Class::method(具体见 prefixed_name 分支)。
6. 标准库与 typemap(Lib/em/)
| 文件 | 作用 |
|---|---|
em.swg |
语言配置入口 |
typemaps.i |
类型映射 |
std_string.i |
std::string 等 |
std_map.i / std_common.i |
容器与公共片段 |
扩展新类型时,应优先通过 typemap 与 ctype/in/out 配合 printFunctionParam 中的 tmap 查找,避免在模块内硬编码。
7. 配置项速查表
| 节点属性(实现读取) | 作用 |
|---|---|
feature:customem:rename |
覆盖 JS 绑定名的基准标识(仍追加 (arg,...) 后缀) |
feature:customem:aysnc |
为方法启用 embind async() |
feature:customem:config |
模板实例类使用自定义注册模板名 |
feature:emsmart |
为类启用 .smart_ptr<mxObject<T>> |
feature:extend |
走 %extend 生成路径 |
doxygen |
填入 .doc("...") |
8. 已知限制与建议
- 智能指针 :依赖
feature:emsmart与mxObject<T>;未自动识别std::shared_ptr等标准 typedef。 customem:aysnc** 拼写**:与英文 async 不一致,新接口文件易写错;长期建议别名或修正为customem:async并保持兼容。- 重命名 + 重载:JS 名仍带参数列表后缀,避免重载冲突。
- 许可 :SWIG 本体以 GPLv3 为主;修改
Source/Modules并再分发 SWIG 二进制须遵守许可证。详见docs/swig-gpl-license-notes.md。
9. 源码索引
| 主题 | 主要位置 |
|---|---|
| 模块入口 / 预处理宏 | EmBind::main() |
| 输出文件初始化、BOM | EmBind::top() |
| 类注册、智能指针、模板 config | EmBind::classHandler() |
| 方法/静态方法/重载/异步/文档 | EmBind::printFunctionParam() |
| JS 绑定名 | EmBind::embindJsBindingName() |
| Director / 虚函数 lambda | EmBind::lambdafunction(),classDirectorMethod() 等 |
| 枚举 | EmBind::enumDeclaration() |
本文档随 Source/Modules/embind.cxx 与 Lib/em/ 变更请及时同步。