这一篇,主要讲解一下 wasm 化中遇到的通用问题,以及通用解,大部分场景我觉得在项目中都遇到了,踩的坑也特别具有代表性,分享一下给有缘人。
rust std
将 rust 项目编译到 wasm,第一步是,尝试下载 wasm32-unknown-unknown
target,然后 cargo c --target wasm32-unknown-unknown
看看报错,接下来就是去解决这些问题,无论是依赖库的问题,还是自身代码的问题,当然,这些问题并不是所有需要解决的,这只是第一步。
rust 的交叉编译不像嵌入式编程,基本都有 std 的实现,但是,有些实现是 panic,这个一定要注意,不是编译通过了,事情就完成了,wasm32-unknown-unknown
的 std,会有非常多 panic 的实现。例如 time/thread/fs,具体代码在这里和这里。
那么这些 panic 实现怎么办,很简单,找替代的库或者自己手写 js 实现,然后绑定到 rust,让实现的接口与 std 一致,再通过 cfg 的条件编译,让它在 wasm32
下启用,在 native 下使用 std 正常实现就行,比如,我们常用的获取 Instant 的接口,社区已经有人实现了这一整套内容,并做成了一个库 web-time,它用的就是 js 获取时间的 API – Performance.now()
。
那么我们需要调用 js/DOM 等浏览器接口,应该怎么办,社区也是有解决方案的,这里不得不提绕不开的三大库:web-sys、js-sys、wasm-bindgen,当然还有在这些库上面的一些封装,比如 gloo,但是,再使用过程中,这些库要么是太底层,很难使用,要么是写起来非常别扭,需要手动绑定 js 到 rust,通过 js Promise 与 rust Future 的相互调用,实现一些 js 容易写,而用 rust 写起来不顺手的东西,这就需要 wasm-bindgen 和 wasm-bindgen-futures 配合起来,将 js 类型绑定到 rust,然后就可以在 rust 实现中用 Future 的写法来调用 Promise 了,并且可以相互转换。
我们编译到 wasm 的代码,是单线程的,在浏览器中用 js 调用 wasm,就相当于跑在浏览器的事件主循环的主线程上,当然,你也可以用 webworker 让它跑在一个其他线程上,但暂时而言,rust 实现的 wasm 只有单线程,虽然据说标准委员会已经提案了 wasm 多线程方案,但只是在浏览器端,这个问题就相当于开放 js 多线程一样难崩。
依赖库
我们做一个项目,有茫茫多的依赖库是很正常的一件事情,但并不是所有依赖库都会考虑交叉编译的问题,尤其是特别支持一些奇怪的平台,但我们编译到 wasm,需要的是所有代码都必须编译过去,这样,就需要对我们的依赖库做一些筛选。一般而言,如果它是纯 rust 实现,那必然能编译,如果有 ffi,就要单独对这个库进行一些测试。常见的比如 secp256k1,虽然它是 ffi 实现,但它支持 wasm 编译,并且工作得很好;而又比如 ring,它也是 ffi 实现,但它在一些特殊的平台就没办法工作了,基本就是三大平台能用的样子。我们必须一点点将这些问题清理干净,才能达到完全编译的效果。
举例说明,比如 tokio,它部分结构是全平台都支持的,少数是不支持的,在它的文档中有对 wasm 支持的说明,我们在 cargo.toml 里就可以这么写:
[dependencies]
tokio = { version = "1", features = ["sync", "macros"] }
[target.'cfg(not(target_family = "wasm"))'.dependencies]
tokio = { version = "1", features = ["rt-multi-thread"] }
这样就能很好隔离一些不支持的 feature。在 ckb on wasm 的实现过程中,我对 ckb 的一些库做了一些微小的改动和适配,基本都是这些改动,PR 在这里,还有一些遗留问题,所有并没有合并进去,后面确定了方案之后,就可以考虑合并了。
tentacle wasm 优化
在整个实现过程中,tentacle 这个库的实现其实也坑了自己不小的时间,主要也是当时在升级 async/await 语法的时候,偷懒了,遗留了问题,导致现在又撞上了,然后不得不解决,于是形成了这个 PR。
这里面有两个问题很影响上层用户体验:
- tentacle 的 trait 在 wasm target 下,强制 no Send
- yamux 在 wasm 下的实现,存在漏洞,即有 panic 的可能性
第一个问题,是因为绑定 js 过程中,把 js 的 ptr 泄漏到了 trait 上,我们都知道默认 ffi 实现都需要用户自己保证它的 Send/Sync,而 js 的 browser 实现其实多线程是没法保证 Send 和 Sync 的,它用 webworker 如果在两个线程使用一个 websocket 都有可能会出现神奇的问题。我的修复很简单,隔离掉 js ptr 的泄漏,让它单独用 spawn_local
跑在当前线程就可以了。
第二个问题,是之前写法上的问题,其实还是 web 端获取时间的问题,现在用 web-time 库实现 wasm32-unknown-unknown
下的时间,然后 wasi 平台用 mock 的方式,但由原来的全局变量改成 session 级别,这样就解决了 panic 的问题。
最后
基本上,按照上述流程去筛一遍想要编译到 wasm 的库,都能解决掉大部分问题,而剩下的,就是不同平台需要不同实现的问题了,比如 web 端的持久化方案,这也是下一篇要说的 indexdb