又是前几天,团队群里发生了如下的对话:

====================================================

同事 A: flask 作者用 rust 写了个 diff 库,https://github.com/mitsuhiko/similar

同事 B:可以用来干啥呢?

同事 A:用来在 rust 生态做 diff

====================================================

我突然想到以前不知道 VSCode 有 compare file with... 和 select for compare 的功能的时候用过一个工具叫 Meld,可以用来对两个或多个文件进行 diff 操作,于是回了一嘴:

====================================================

比如拿 rust 写个 meld,再通过 web assembly 运行在浏览器

====================================================

回完没过多久,就想打自己一巴掌,怎么那么手贱呢?非要给自己搞点事情。

怎么就管不住这手呢.gif

于是就开搞了。

我的想法很简单:找到一个工具可以将写好的 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 代码有一步 FileUint8Array 的转换,因为不熟悉 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 之外的部分,简单得有些让人意外......

注:文章对话内容部分有重新排版。

作者的碎碎念:最近竟然被人说文章思路很清晰......意外到怀疑是在讽刺,我觉得我自己都不会想看我写过的文章......甚至一边写一边怀疑这个东西写出来到底有什么用(也在考虑单独写一篇文章讨论这个事情的意义)

参考

  1. wasm-pack book
  2. wasm-opt
  3. Meld
  4. similar
  5. 清华大学开源软件镜像站
  6. TextEncode、Blob、FileReader → MDN