只要用
rust
写过wasm
肯定都用过wasm-bindgen
, 我就不再介绍wasm-bindgen
了,pr 在这里 #3901
起因
我最近正在用 Rust
写我的物理引擎(正在写文档,很快就能开源出来了),需要用 wasm-bindgen
迁移到 web
里面,有一些 ts 类型需要自定义,很尴尬的是,wasm-bindgen
不支持导入外部文件引入自定义类型。
你必须使用常量字符,串举例来说,下面的代码是合法的,
rust
use wasm_bindgen::prelude::*;
// 自定义你的 ts 类型
#[wasm_bindgen(typescript_custom_section)]
const _: &str = "type Picea = { free: VoidFunction }";
如果你想拆分这段类型到独立的文件,使用 include_str
或者任何形式的表达式,都是不可以的
这能忍?但是看了下 wasm-bindgen
的源码后,我发现问题并不简单
这里必须要解释一下 wasm-bindgen
到底做了什么, 有兴趣的朋友可以直接去看源代码,我简单解释一下
wasm-bindgen 的流程
首先 wasm-bindgen
作为属性宏会去解析这段代码块,遍历它的抽象语法树,根据不同的类型提取不同的信息,
比如当你绑定在一个函数上面的时候,会去提取你的函数名等信息,后面用于生成对应的 js 代码
又比如当你绑定在上面的的一个常量并使用了 typescript_custom_section
属性的时候,会提取后面的常量字符串
下面是 wasm-bindgen
对不同类型的代码的处理部分
这些提取出来的信息都放在 Program
对象里面,你可以理解成它是对于 ast 抽象语法树的简化。里面包含了导入导出的一些信息,还有我们要处理的 typescript_custom_sections
字段
之后要对这个 Program
对象进行序列化,有编解码经验的同学应该知道,就是将 Program
转换成字节序列
说白了把这个对象当成一个树,深度优先遍历这个
Program
对象,然后对每一个访问的字段进行序列化(转换成字节或者字节数组)最后合并起来拼凑一个字节序列
我们还是拿上面的例子说明一下,这个效果最终是怎么样的
假设你的代码如下所示
rust
use wasm_bindgen::prelude::*;
#[wasm_bindgen(typescript_custom_section)]
const _: &str = "type Picea = { free: VoidFunction }";
使用 cargo expand
看一下效果,注意一定要加 target ,不然你啥都看不到
没有使用过
cargo expand
的同学要安装一下,写rust
必备
bash
cargo expand --target wasm32-unknown-unknown
可以看到,经过宏转换后,上面说的 Program
被序列化在 pub static _GENERATED: [u8;134usize]
这个字节数组里面,并且打了一个 #[link_section]
的标签,这里面刚好有我们要添加的 typescript 的类型代码,也有一些 schema_version
的信息
总结一下,就是 Program
的数据被序列成字节数组, 然后一同编译进入最终的产物里面
wasm-pack
后面一般都是用 wasm-pack
直接进行打包的, 有兴趣的同学可以直接去看源代码,但是实际上 wasm-pack
没做什么事情,里面还是在调用 wasm-bindgen cli
工具,所以还是回到 wasm-bindgen
, 去看一下 cli
做了啥
wasm-bindgen cli
其实二者通用了一个结构体类型Program
,基于这个类型进行编解码,然后 cli
在提取到需要的信息后,生成对应的 js ,ts
文件,后面的部分有兴趣自己去看下吧
小结
总结一下所有的步骤
wasm-bindgen 宏
解析抽象语法树,提取重要信息,生成一个小的ast::Program
树结构- 之后对
ast::Program
序列化,放在编译的产物里面 wasm-bindgen cli
反序列化生成Program
对象,提取对应的信息,生成js,ts
文件
好了,知道这些,就可以来解决上面的问题了
宏展开的难题
为什么不支持表达式, 因为宏解析的过程不能解析表达式,比如你写了如下的代码,右边是一个表达式
rust
use wasm_bindgen::prelude::*;
#[wasm_bindgen(typescript_custom_section)]
const _: &str = include_str!("./types.d.ts");
在宏展开的时候,你可以判断右边是否是 include_str
然后进一步读取路径对应的文件的信息,这一步相当于你要实现 include_str
宏的功能。
社区已经有人提了类似的 pr
, 但是被否决了,因为不可能枚举所有的情况,假设右边是一个变量呢
rust
use wasm_bindgen::prelude::*;
const TYPES: &str = "type Picea = { free: VoidFunction }";
#[wasm_bindgen(typescript_custom_section)]
const _: &str = TYPES; // 这里没办法在宏展开的时候解析对应的内容。。。
解决方案
那怎么解决这个问题呢?用 const
将编码 Program
的逻辑在编译期间实现,这样 rustc
就可以帮我们做完所有的解析工作了,我们只需要编写const
函数就可以了
非常感谢 71 大佬, 主要的思路都来源于他
更具体一点就是在宏展开的时候收集表达式
然后在编码的时候判断
- 如果是表达式,生成
const 的代码
在编译期间编码成字节数组,然后在编译期拼接到_GENERATED
里面 - 如果是其他已经被编码过的字节数组,在编译期拼接到
_GENERATED
里面
这样就可以完美的解决这个问题,再看下cargo expand
出来了什么
通过一些 const function
实现编译期间的序列化工作
总结
目前 wasm-bindgen
还没更新, 如果你要使用该功能,可以暂时使用我的patch
rust
[patch.crates-io]
wasm-bindgen = { git = 'https://github.com/swnb/wasm-bindgen.git' }
最后还是要呼吁一下大家积极参与 rust
生态的建设 ,因为参与者太少了,这些问题已经遗留有几年了,说到底还是 rust
的开发者数量和规模不够导致的