- 开发工具
【免费下载链接】wasm-bindgen
Facilitating high-level interactions between Wasm modules and JavaScript
本文是 wasm-bindgen 设计系列(guide/src/contributing/design/index.md)中的一篇,聚焦于 Rust/JS 两侧类型信息是如何被编码、传输并最终被wasm-bindgenCLI 工具还原的。读完本文,你将完整掌握#[wasm_bindgen]宏如何把"类型签名"编译为可执行函数、如何通过__wbindgen_describe导入把一串u32送达宿主侧,以及enum Descriptor如何把这条流式编码重新解析成 CLI 可用的完整类型树。
一、问题背景:为什么类型信息必须"运行时"传递
在 Rust 与 JS 之间转换类型时,最棘手的问题不是转换本身,而是"如何把类型信息从 Rust 编译器这边传达到 wasm-bindgen CLI 那边"。
#[wasm_bindgen]宏运行在 Rust 代码的**语法层(syntactical、未解析的结构)**之上,它需要生成信息供后续的wasm-bindgenCLI 工具读取。这里存在两个天然的障碍:
- 编译期不可知:像关联类型投影(associated type projections)和 typedef 这类东西,它们的"具体是什么类型"要等编译器推进到很后面的阶段才能确定,宏在语法层根本拿不到完整答案;
- 类型很"富":CLI 需要理解诸如
FnMut(String, Foo, &JsValue)这样的组合类型,这类带闭包、带结构体、带引用语义的签名,靠简单的文本或名称列表根本无法表达。
因此 wasm-bindgen 采取了一个略微"不走寻常路"(unconventional)的方案:静态信息用 JSON 序列化(写入 Wasm 可执行文件的 custom section),而动态类型信息则通过"可执行函数"来携带。这就是本文的主角WasmDescribetrait 与 descriptor(描述符)机制。
从源码结构看,这条管线的两端分别位于:
- Rust 侧(宏生成 + trait 实现):src/describe.rs —— 定义
WasmDescribetrait 及其各种基础类型的实现; - CLI 侧(解析还原):crates/cli-support/src/descriptor.rs —— 定义
enum Descriptor及流式解码逻辑,以及 crates/cli-support/src/descriptors.rs —— 执行并收集所有描述符函数的结果。
二、WasmDescribetrait:为每个类型写一份"签名程序"
整个机制的核心是一个看似朴素实则关键的 trait:
pub trait WasmDescribe { fn describe(); }这个定义位于 src/describe.rs。它没有返回值、没有参数,唯一的作用是"在被调用时,把当前类型的编码信息逐个写入宿主"。配合一个底层入口函数:
#[inline(always)] pub fn inform(a: u32) { unsafe { super::__wbindgen_describe(a) } }inform把每一个u32通过__wbindgen_describe这个 Wasm 导入传给宿主(即 CLI 侧的wasm-interpreter)。所有基础类型都以极其直接的方式实现该 trait:
simple! { i8 => I8 u8 => U8 i16 => I16 u16 => U16 i32 => I32 u32 => U32 i64 => I64 u64 => U64 i128 => I128 u128 => U128 f32 => F32 f64 => F64 bool => BOOLEAN char => CHAR JsValue => EXTERNREF }(完整映射见 src/describe.rs)。注意这里存在一个架构相关的分支:isize/usize在 wasm32 上映射为I32/U32,在 wasm64 上则映射为I64_AS_F64/U64_AS_F64(见 src/describe.rs)。字符串也随 feature 开关变化:开启enable-interning时str/String映射为CACHED_STRING,否则为STRING(src/describe.rs)。
复合类型的递归描述
真正体现设计巧思的是复合类型:描述不是拍平的一串数字,而是带结构的递归调用。例如:
- 引用
&T:先发REF,再递归描述T;&mut T同理发REFMUT(src/describe.rs); - 切片
[T]:先发SLICE,再递归描述T(src/describe.rs); - 向量
Vec<T>/Box<[T]>:经由WasmDescribeVector先发VECTOR再递归(src/describe.rs); - 可选值
Option<T>:先发OPTIONAL再递归(src/describe.rs); - 结果
Result<T, E>:先发RESULT再递归描述成功分支(src/describe.rs); - 钳制数
Clamped<T>:先发CLAMPED再递归(src/describe.rs)。
也就是说,describe()的执行过程本身就是一个"深度优先遍历",每个inform(u32)是一次编码动作,递归调用则展开子类型。最终宿主编译器拿到的就是一条前序遍历产生的扁平u32流。
三、宏如何生成__wbindgen_describe_*函数
除了前文提到的 JS shim 之外,宏还会为每个导出/导入额外生成一个描述函数。以文档中的导出为例:
#[wasm_bindgen] fn greet(a: &str) { // ... }宏在生成普通 shim 的同时,还会生成类似这样的东西:
#[no_mangle] pub extern "C" fn __wbindgen_describe_greet() { <dyn Fn(&str)>::describe(); }(这段示意代码来自设计文档原文。实际代码生成逻辑在 crates/macro-support/src/codegen.rs:描述函数的命名规则是__wbindgen_describe_前缀拼接 wasm 符号名,CLI 侧通过剥离前缀来把描述符与对应的 shim 关联起来,见 crates/macro-support/src/codegen.rs 与 crates/cli-support/src/descriptors.rs)。
描述函数的生成覆盖了完整的功能面:
- 导出函数:对每个参数依次
<ty as WasmDescribe>::describe(),对返回值调用<ret_ty as WasmDescribe>::describe()与<inner_ret_ty as WasmDescribe>::describe()(后者是内部返回类型,见 crates/macro-support/src/codegen.rs); - 导入函数:同样的模式,参数描述见 crates/macro-support/src/codegen.rs;
- 导入类型(class):为每个导入的 JS 类实现
WasmDescribe(crates/macro-support/src/codegen.rs); - Rust 结构体导出:为
#[wasm_bindgen]导出的结构体实现WasmDescribe(crates/macro-support/src/codegen.rs); - 枚举与字符串枚举:生成描述实现(crates/macro-support/src/codegen.rs、crates/macro-support/src/codegen.rs);
- getter/setter:字段访问器也各自带一个描述函数(crates/macro-support/src/codegen.rs)。
除此之外,还有一类**按单态化(per-monomorphisation)**生成的描述函数:泛型导入与wbg_cast恒等适配器通过__wbindgen_describe_generic_import标记导入,把(func 指针, ABI 参数指针)传给宿主(src/describe.rs),让 CLI 能按每个具体单态实例制造 JS 绑定(crates/cli-support/src/wit/mod.rs)。
四、__wbindgen_describe导入:一条通往宿主的u32流
当wasm-bindgenCLI 运行时,它会执行这些描述函数。执行的落地机制是:
每次调用
__wbindgen_describe会向宿主传递一个u32;多次调用累积起来,就等效于得到一条Vec<u32>。
这条Vec<u32>随后被重新解析成enum Descriptor,从而完整地描述一个类型。这里要强调的是:这些执行发生在 CLI 侧的 Wasm 解释器(interpreter)中,而不是浏览器或 Node 里。从 crates/cli-support/src/descriptors.rs 的模块注释可以看到整个流程:
- 拿到 rustc 产出的原始 Wasm 模块;
- 找出所有以
__wbindgen_describe_为前缀的导出函数,逐个解释执行(execute_exports,crates/cli-support/src/descriptors.rs); - 同时扫描所有直接调用
__wbindgen_describe_generic_import标记的函数(execute_generic_imports,crates/cli-support/src/descriptors.rs); - 用
Descriptor::decode把每条u32流还原成描述符(crates/cli-support/src/descriptor.rs); - 把结果放进一个新的 custom section
WasmBindgenDescriptorsSection(crates/cli-support/src/descriptors.rs),随后 CLI 在处理程序时按需取用(crates/cli-support/src/wit/mod.rs)。
值得注意的是宏还通过#[link_section = "__wasm_bindgen_unstable"]写入了一个 custom section,用于存放 JSON 序列化的静态结构信息(schema 版本、AST 编码等),见 crates/macro-support/src/codegen.rs。
五、enum Descriptor:解码后的类型树
CLI 侧的解码目标是一个庞大的判别式枚举,完整定义在 crates/cli-support/src/descriptor.rs:
pub enum Descriptor { I8, U8, ClampedU8, I16, U16, I32, U32, I64, U64, I64AsF64, U64AsF64, I128, U128, F32, F64, Boolean, Function(Box<Function>), Closure(Box<Closure>), Ref(Box<Descriptor>), RefMut(Box<Descriptor>), Slice(Box<Descriptor>), Vector(Box<Descriptor>), CachedString, String, Externref, NamedExternref(String), Enum { name: String, hole: u32, unique_crate_identifier: String }, StringEnum { name: String, invalid: u32, hole: u32 }, DynamicUnion { name: String, variant_types: Vec<Descriptor> }, RustStruct { name: String, unique_crate_identifier: String }, Char, Option(Box<Descriptor>), Result(Box<Descriptor>), Unit, NonNull, RawPointer, }可以看到它和 Rust 侧的inform(u32)编码一一对应:每个变体要么是标量,要么携带递归子描述符,形成一棵类型树。带名称的类型(枚举、结构体、命名 externref)还会携带name与unique_crate_identifier,用于在后续阶段解析为最终的 JS 标识(见 crates/cli-support/src/descriptor.rs 的visit_named_types_mut)。
Function与Closure是树中最复杂的节点:
pub struct Function { pub arguments: Vec<Descriptor>, pub shim_idx: u32, pub ret: Descriptor, pub inner_ret: Option<Descriptor>, } pub struct Closure { pub owned: bool, pub function: Function, pub mutable: bool, }(crates/cli-support/src/descriptor.rs)。Function携带参数表、shim 索引、返回类型及内部返回类型,这让 CLI 能还原出FnMut(String, Foo, &JsValue)这样的富类型签名——闭包描述符额外记录所有权与可变性。
流式解码算法
解码是一个简单的递归前序消费过程(_decode,crates/cli-support/src/descriptor.rs):
- 读一个
u32作为标签,匹配到对应变体; - 标量类型直接结束;
- 复合类型(
REF/REFMUT/SLICE/VECTOR/OPTIONAL/RESULT)继续递归读取一个子描述符; - 带名称的类型继续读长度前缀的字符串(
get_string先读字符数再逐个读u32字符,crates/cli-support/src/descriptor.rs); CLAMPED会设置一个clamped标志后递归,使内层的U8被解码为ClampedU8(crates/cli-support/src/descriptor.rs);- 解码结束后断言数据已消费完毕(
assert!(data.is_empty(), ...)),任何未知标签都会触发panic!("unknown descriptor: {other}")——这说明宏与 CLI 之间依赖严格的协议对齐。
编码标签本身定义在共享 crate 中,crates/shared/src/tys.rs 用宏连续编号生成全部 31 个常量:I8, U8, ..., RAW_POINTER。宏侧写inform($d)、CLI 侧读u32标签,两侧共用同一份常量表,从协议层面保证了编码/解码的一致性。
六、执行代价为零:描述函数最终被裁剪
设计文档特别强调了这个机制的一个关键特性:
整体上这套方案有些迂回(roundabout),但对生成代码和运行时没有任何影响。所有描述函数都会从最终产出的 Wasm 文件中被裁剪掉。
从源码可以确认这句话的落实:
execute()在解释完所有描述函数后,会把它们从模块中删除(crates/cli-support/src/descriptors.rs 删除导出项,模块注释亦说明"All descriptor functions are removed after this pass runs");- 搜索结果中 crates/cli-support/src/wit/mod.rs 检查模块中是否存在以
__wbindgen_describe开头的导出项,用于校验逻辑; - CLI 会删除
__wbindgen_describe与__wbindgen_describe_generic_import这两个特殊导入(crates/cli-support/src/wit/mod.rs)。
因此,描述机制是纯编译期/工具链期的存在:它只影响wasm-bindgen工具对中间产物的后处理,用户最终拿到的foo.js+foo_bg.wasm中完全不含这些描述函数的痕迹。
七、整条管线串联:从 Rust 源码到 JS 绑定
把以上各部分串起来,一个#[wasm_bindgen]函数从写出来到被 JS 调用的完整数据流是:
- 宏展开:
#[wasm_bindgen]宏在语法层解析函数签名,生成三类产物——普通 JS shim、__wbindgen_describe_<name>描述函数、以及 JSON 编码的 AST 静态信息(写入__wasm_bindgen_unstablecustom section),见 crates/macro-support/src/codegen.rs; - 编译:rustc 产出 Wasm 文件,其中描述函数以
#[no_mangle]导出符号存在,其函数体是对WasmDescribe::describe()的一连串调用; - CLI 解释执行:
wasm-bindgen工具用内置的 Wasm 解释器逐个执行这些描述函数,__wbindgen_describe导入把每个u32交给宿主侧累积成Vec<u32>; - 解码建树:
Descriptor::decode把u32流还原为enum Descriptor类型树,存入WasmBindgenDescriptorsSectioncustom section; - 消费与裁剪:CLI 在处理导入/导出时按 shim 名取出对应描述符(
self.descriptors.remove(&wasm_name),crates/cli-support/src/wit/mod.rs),据此生成正确的 JS 绑定与 ABI 适配代码,最后删除全部描述函数和描述相关导入。
测试方面,仓库在 crates/cli-support/src/descriptor.rs 提供了一个单元测试vector_kind_accepts_memory64_scalar_descriptors,验证 wasm64 场景下U64AsF64/I64AsF64标量描述符也能被vector_kind()正确识别为U64/I64向量——可以直接运行cargo test -p wasm-bindgen-cli-support来验证解码逻辑。
八、小结
WasmDescribe机制是 wasm-bindgen 架构中最具独创性的一环:它把"类型签名"这种编译期无法穷尽的动态信息,编码为一组可以执行的描述函数,借助一个朴素的u32通道传送到 CLI,再用一棵递归的Descriptor树还原出完整、精确、可机读的类型结构。这一设计既绕开了宏只能看到语法层的限制,又完全避免了描述代码对最终产物的任何运行期影响——执行完即裁剪,静默而高效。
如果想深入体系地理解 wasm-bindgen 的设计,可以继续阅读同目录下的姊妹篇:rust-type-conversions.md(Rust 类型转换)与 index.md(整体设计导览);guide/src/contributing/design/exporting-rust.md 与 guide/src/contributing/design/importing-js.md 则分别从导出、导入视角展示宏生成的完整图景。
- 开发工具
【免费下载链接】wasm-bindgen
Facilitating high-level interactions between Wasm modules and JavaScript
相关推荐
Ruff 描述符协议(Descriptor Protocol)类型推断深度解析
Ruff 描述符协议(Descriptor Protocol)类型推断深度解析 导读 本文以 Ruff 仓库中类型检查器(ty)的类型推断测试规范文档 desc
开发工具Lint格式化静态分析CLIGel/EdgeDB 二进制协议类型描述符(Type Descriptor)完全解析:从 CommandDataDescription 到编解码器
Gel/EdgeDB 二进制协议类型描述符(Type Descriptor)完全解析:从 CommandDataDescription 到编解码器 本指南系统讲
数据库图数据库关系型数据库RenderDoc 描述符与绑定深度解析:从 Descriptor Abstraction 到各 API 底层实现
RenderDoc 描述符与绑定深度解析:从 Descriptor Abstraction 到各 API 底层实现 导读 本文基于 RenderDoc 官方 P
开发工具调试器图形学GPU
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考