一次给 wasm-bindgen 提 pr 的经历,在自定义 ts 类型的时候使用表达式
只要用
rust写过wasm肯定都用过wasm-bindgen, 我就不再介绍wasm-bindgen了,pr 在这里 #3901
起因
我最近正在用 Rust 写我的物理引擎(正在写文档,很快就能开源出来了),需要用 wasm-bindgen 迁移到 web 里面,有一些 ts 类型需要自定义,很尴尬的是,wasm-bindgen 不支持导入外部文件引入自定义类型。
你必须使用常量字符,串举例来说,下面的代码是合法的,
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对象,然后对每一个访问的字段进行序列化(转换成字节或者字节数组)最后合并起来拼凑一个字节序列
我们还是拿上面的例子说明一下,这个效果最终是怎么样的
假设你的代码如下所示
use wasm_bindgen::prelude::*;
#[wasm_bindgen(typescript_custom_section)]
const _: &str = "type Picea = { free: VoidFunction }";
使用 cargo expand 看一下效果,注意一定要加 target ,不然你啥都看不到
没有使用过
cargo expand的同学要安装一下,写rust必备
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文件
好了,知道这些,就可以来解决上面的问题了
宏展开的难题
为什么不支持表达式, 因为宏解析的过程不能解析表达式,比如你写了如下的代码,右边是一个表达式
use wasm_bindgen::prelude::*;
#[wasm_bindgen(typescript_custom_section)]
const _: &str = include_str!("./types.d.ts");
在宏展开的时候,你可以判断右边是否是 include_str 然后进一步读取路径对应的文件的信息,这一步相当于你要实现 include_str 宏的功能。
社区已经有人提了类似的 pr, 但是被否决了,因为不可能枚举所有的情况,假设右边是一个变量呢
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
[patch.crates-io]
wasm-bindgen = { git = 'https://github.com/swnb/wasm-bindgen.git' }
最后还是要呼吁一下大家积极参与 rust 生态的建设,因为参与者太少了,这些问题已经遗留有几年了,说到底还是 rust 的开发者数量和规模不够导致的
转载自:https://juejin.cn/post/7355763456082657320




