likes
comments
collection
share

一次给 wasm-bindgen 提 pr 的经历,在自定义 ts 类型的时候使用表达式

作者站长头像
站长
· 阅读数 7

只要用 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 提 pr 的经历,在自定义 ts 类型的时候使用表达式

这能忍?但是看了下 wasm-bindgen 的源码后,我发现问题并不简单

这里必须要解释一下 wasm-bindgen 到底做了什么, 有兴趣的朋友可以直接去看源代码,我简单解释一下

wasm-bindgen 的流程

首先 wasm-bindgen 作为属性宏会去解析这段代码块,遍历它的抽象语法树,根据不同的类型提取不同的信息,

比如当你绑定在一个函数上面的时候,会去提取你的函数名等信息,后面用于生成对应的 js 代码

又比如当你绑定在上面的的一个常量并使用了 typescript_custom_section 属性的时候,会提取后面的常量字符串

下面是 wasm-bindgen 对不同类型的代码的处理部分

一次给 wasm-bindgen 提 pr 的经历,在自定义 ts 类型的时候使用表达式

这些提取出来的信息都放在 Program 对象里面,你可以理解成它是对于 ast 抽象语法树的简化。里面包含了导入导出的一些信息,还有我们要处理的 typescript_custom_sections 字段

一次给 wasm-bindgen 提 pr 的经历,在自定义 ts 类型的时候使用表达式

之后要对这个 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

一次给 wasm-bindgen 提 pr 的经历,在自定义 ts 类型的时候使用表达式

可以看到,经过宏转换后,上面说的 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

一次给 wasm-bindgen 提 pr 的经历,在自定义 ts 类型的时候使用表达式

其实二者通用了一个结构体类型Program,基于这个类型进行编解码,然后 cli 在提取到需要的信息后,生成对应的 js ,ts 文件,后面的部分有兴趣自己去看下吧

小结

总结一下所有的步骤

  1. wasm-bindgen 宏 解析抽象语法树,提取重要信息,生成一个小的ast::Program树结构
  2. 之后对ast::Program 序列化,放在编译的产物里面
  3. 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 大佬, 主要的思路都来源于他

更具体一点就是在宏展开的时候收集表达式

一次给 wasm-bindgen 提 pr 的经历,在自定义 ts 类型的时候使用表达式

然后在编码的时候判断

  1. 如果是表达式,生成 const 的代码 在编译期间编码成字节数组,然后在编译期拼接到 _GENERATED 里面
  2. 如果是其他已经被编码过的字节数组,在编译期拼接到 _GENERATED 里面

一次给 wasm-bindgen 提 pr 的经历,在自定义 ts 类型的时候使用表达式

这样就可以完美的解决这个问题,再看下cargo expand 出来了什么

一次给 wasm-bindgen 提 pr 的经历,在自定义 ts 类型的时候使用表达式

通过一些 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
评论
请登录