又是前几天,团队群里发生了如下的对话:
====================================================
同事 B:可以用来干啥呢?
同事 A:用来在 rust 生态做 diff
====================================================
我突然想到以前不知道 VSCode 有 compare file with... 和 select for compare 的功能的时候用过一个工具叫 Meld,可以用来对两个或多个文件进行 diff 操作,于是回了一嘴:
====================================================
====================================================
回完没过多久,就想打自己一巴掌,怎么那么手贱呢?非要给自己搞点事情。
于是就开搞了。
我的想法很简单:找到一个工具可以将写好的 rust 编译成 wasm,然后通过 script 调用,但实操才发现,这东西做起来竟然比我想象的还要简单!rust 社区已经有了完备的工具 wasm-pack,而且还有模版和教程,让你 rust 零基础上手,简直是 😯。
小插曲:安装 wasm-pack
我电脑上已经安装了 rust,所以想着直接装 wasm-pack 就可以,官网表示,安装这玩意儿只需要一句命令:
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh
但是没过一会儿就提示找不到 rustup,就去翻了一下 rust 官网,发现安装页面已经直接推荐使用 rustup 管理 rust 的安装了,so 删掉系统内的 rust(可能是用 homebrew 安装的,已经不记得了),重新安装 rustup。
安装 rustup 和使用 cargo(npm for rust)的过程中,还是遇到了网络问题(🤦♂️ 每一个国外命令行工具都绕不过的痛),想到以前给树莓派安装系统的时候也遇到过网络问题,然后搜教程的过程中发现了清华大学开源软件镜像站,就顺手搜了一下 rust,没想到还真有 rustup 和 crates 的镜像,于是就切换到了国内,速度瞬间酸爽无比 😊。
rust 上手
说是 rust 上手,其实只是把 wasm-game-of-life 下下来看眼代码而已。wasm-game-of-life 是 wasm-pack 提供的模版,用来让大家学习使用 rust 编写一个生命游戏。模版里的代码很简单,核心只有 Cargo.toml 和 src/lib.rs 文件。Cargo.toml 看名称就知道是 cargo 读取的文件,类似 package.json,放一些项目和依赖配置;src/lib.rs 的文件内容也很简单:
mod utils;
use wasm_bindgen::prelude::*;
// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
#[wasm_bindgen]
extern {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello, {{project-name}}!");
}
我们需要关注的只有最下面的八行。
wasm_bindgen
这八行代码中有两行代码是相同的,而且都很特殊:#[wasm_bindgen]
,这其实是 wasm_bindgen 的写法,wasm_bindgen 是一个方便在 rust 和 JavaScript 之间进行交互的库,简单理解就是,使用了它之后,我们可以快速在 JavaScript 和 rust 之间进行协调,互相暴露方法给对方使用。
这里的 extern
,其实就是将 alert
方法提供给 rust;而 greet
方法,会被转换为 wasm 方法,JavaScript 引入 wasm 模块之后开箱即用。
简易 Meld
因为是尝鲜,我们就将 Meld 的流程最简化:上传文件 → rust 做 diff → JavaScript 显示。既然已经知道了 rust 如何编译成 wasm,那剩下就是在 rust 和 JavaScript 之间传递文件和 diff 内容了。
简单翻了一下 rust 的教程,就看到了一个熟悉的名字 u8
,这个跟 Uint8Array
好像有点关系,而 File
肯定有可以跟 Uint8Array
做转换的方法,于是先写个 demo 尝试一下,看能不能使用这个数据类型传递。
#[wasm_bindgen]
pub fn test_param(a: &[u8]) {
}
wasm-pack build
一下,然后通过 JavaScript 调用,
import * as wasm from 'rust-meld';
wasm.test_param(new TextEncoder('').encode('测试'));
没什么问题,但返回值呢? u8
果然失败了,于是只能换一个类型,万能的 String
!不管行不行,先来一把,代码换成:
#[wasm_bindgen]
pub fn test_param(a: &[u8]) -> String {
return String::from("测试 String");
}
Bingo!没有问题!
简易流程走通了,剩下的其实就很简单了,rust 代码就不贴了,similar 的 demo 拿来改改就可以用了,想要代码的童鞋可以直接找我,或者邮件索取。评论区最近正在开发中,敬请期待!
File to Uint8Array
JavaScript 代码有一步 File
到 Uint8Array
的转换,因为不熟悉 API,第一版写法是这样的:
const readFile = (file) => new Promise((resolve, reject) => {
const fileReader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = () => resolve(reader.result);
reader.onerror = () => reject(reader.error);
});
const uint8ArrayOfFile = new Uint8Array(await readFile(file));
后来仔细翻了一下 API 才知道,Blob
原来有 arrayBuffer
的异步方法,可以直接转换成 ArrayBuffer
,所以上面的代码可以简化成:
const uint8ArrayOfFile = new Uint8Aray(await file.arrayBuffer());
wasm-pack 的坑
wasm-pack build 命令的最后一步是使用 wasm-opt 生成体积更小的包,但是意外的是,wasm-pack 并不会去找系统已经存在的 wasm-opt 包,而是尝试自己去下载特定版本的 binary release,感觉有点奇怪,作者好像有提到说 wasm-pack 和 wasm-opt 版本必须匹配才能用,但是这个下载的过程可能会因为网络原因 or 不可知原因一直不动(实际体验:上了个厕所+洗了个澡回来依旧没结束)。所以如果 wasm-pack build 有卡住的话,请将以下代码加入 Cargo.toml,关闭 wasm-opt 功能(强者可手动 opt)。
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
结语
除了 wasm-opt debug 之外的部分,简单得有些让人意外......
注:文章对话内容部分有重新排版。
作者的碎碎念:最近竟然被人说文章思路很清晰......意外到怀疑是在讽刺,我觉得我自己都不会想看我写过的文章......甚至一边写一边怀疑这个东西写出来到底有什么用(也在考虑单独写一篇文章讨论这个事情的意义)
参考
- wasm-pack book
- wasm-opt
- Meld
- similar
- 清华大学开源软件镜像站
- TextEncode、Blob、FileReader → MDN