Rust打包为WASM 应用于JavaScript前端
背景
在前端开发中,我们经常会遇到一些性能瓶颈,比如一些复杂的计算,或者一些需要大量计算的算法,这时候我们可以考虑使用WebAssembly来提高性能。
WebAssembly是一种新的低级字节码格式,可以在现代浏览器中运行,它可以让我们在浏览器中运行其他语言编写的代码,比如C、C++、Rust等。
Rust 因为其安全性可靠性以及高性能,以及跨平台的特性,非常适合用来编写WebAssembly模块。
1 | ### [Rust 和 WebAssembly 用例](https://developer.mozilla.org/zh-CN/docs/WebAssembly/Guides/Rust_to_Wasm#rust_%E5%92%8C_webassembly_%E7%94%A8%E4%BE%8B#rust_和_webassembly_用例) |
MDN网站上本身有一个简明的教程,用于介绍如何将Rust编写的程序打包为WebAssembly模块,然后在JavaScript中调用:编译 Rust 为 WebAssembly - WebAssembly | MDN
MDN这篇文章写的非常通俗易懂,基本上是面向于零基础的读者, 但是问题是,这篇文章里面仍然有很多容易踩的坑,所以说,我在这结合我自己的实践、以及所遇到的问题,来写一篇更加详细的教程。
所以说这篇文章的大部分内容将会和 MDN 的文章一致,但是我会对其的一些内容进行删改,以及添加一些我认为有必要的内容。
路径以及阶段性目标
在本教程中,我们将使用 Rust 的 npm 包构建工具 wasm-pack
来构建一个 npm 包。这个包只包含 WebAssembly 和 JavaScript
代码,以便包的用户无需安装 Rust 就能使用。他们甚至不需要知道这里包含
WebAssembly!
之后我们会使用 vite
框架来引入这个npm包,然后在前端项目中调用这个WebAssembly模块。
安装 Rust 环境
让我们看看安装 Rust 环境的所有必要步骤。
安装 Rust
前往 Install
Rust 页面并跟随指示安装
Rust。这里会安装一个名为“rustup”的工具,这个工具能让你管理多个不同版本的
Rust。默认情况下,它会安装用于惯常 Rust 开发的 stable 版本 Rust
Release。Rustup 会安装 Rust 的编译器 rustc
、Rust
的包管理工具 cargo
、Rust 的标准库 rust-std
以及一些有用的文档 rust-docs
。
备注: 需要注意,在安装完成后,你需要把 cargo 的
bin
目录添加到你系统的
PATH
。一般来说它会自动添加,但需要你重启终端后才会生效。
wasm-pack
要构建我们的包,我们需要一个额外工具
wasm-pack
。它会帮助我们把我们的代码编译成 WebAssembly
并制造出正确的 npm
包。使用下面的命令可以下载并安装它:
1 | cargo install wasm-pack |
可选:安装 Node.js 并获取 npm 账户
在这个例子中我们将会构建一个 npm 包,因此你需要确保安装 Node.js 和 npm 已经安装。
另外,我们将会把包发布到 npm 上,因此你还需要一个 npm 账号。它们是免费的。发布这个包并不是必须的,但是发布它非常简单,因此在本例中我们默认你会发布这个包。
这一步主要是为了,让你能够在本地测试你的WebAssembly模块,以及发布到npm上。之后其他的项目可以直接通过npm来同步更新你的WebAssembly模块,而无需手动操作。
在 Get npm! 页面按照说明下载并安装 Node.js 和 npm。在选择版本时,选择一个你喜欢的版本;本例不限定特定版本。
在 npm signup page 注册 npm 账户,并填写表格。
接下来,在命令行中运行 npm adduser
:
1 | > npm adduser |
你需要完善你的用户名,密码和邮箱。如果成功了,你将会看到:
1 | Logged in as yournpmusername on https://registry.npmjs.org/. |
在这里你可能会遇到两个问题: 1. 你被重定向到了 cnpm 而不是 npm 的网站。cnpm不支持公开账号的注册,它只允许你注册一个私人账号。
遇到这个问题是因为你之前可能遇到过 npm 无法安装某些模块的问题,之后在教程的指引下,使用了npmmirrow 淘宝镜像作为 npm 的源,当更改npm的源的时候,账号验证的源也会被设置为淘宝的镜像源。
解决方式有两个,一个是直接切换npm的源,但是这样,当你下次又有无法安装的而模块的时候,你又需要切换回来,最好的办法是自己手动指定一下登入账号使用的源:
1 | npm login --registry http://registry.npmjs.com |
- 密码和账号输入时无法确认是否输入正确且无法复制粘贴。
使用
1 | npm login --username <username> --password <password> --email <email> |
可以在命令行就指定这些参数,方便检查有无出错。 但是请注意请不要将这行命令添加到公开的文件里,以保护你的账号安全。
构建我们的 WebAssembly npm 包
万事俱备,来创建一个新的 Rust 包吧。打开你用来存放你私人项目的目录,做这些事:
1 | $ cargo new --lib hello-wasm |
这里会在名为 hello-wasm
的子目录里创建一个新的库,里面有下一步之前你所需要的一切:
1 | +-- Cargo.toml |
首先,我们有一个 Cargo.toml
文件,这是我们配置构建的方式。如果你用过 Bundler 的 Gemfile
或者 npm 的 package.json
,你应该会感到很熟悉。Cargo
的用法和它们类似。
接下来,Cargo 在 src/lib.rs
生成了一些 Rust 代码:
1 |
|
我们完全不需要使用这些测试代码,所以继续吧,我们删掉它。
来写点 Rust 代码吧!
让我们在 src/lib.rs
写一些代码替换掉原来的:
1 | extern crate wasm_bindgen; |
上面是MDN给的代码,它在后面给出了每个部分的解释,你可以点击这里跳转到原文进行查看:来写点 Rust 代码吧!
不过我认为将注释和代码放在一起会更方便于参考,读者可以自行决定使用哪种方式。这里将会给出一个缺少部分细节的高级说明;如果想要了解更多 Rust 知识,请查看在线书籍 The Rust Programming Language。
1 | /* |
把我们的代码编译到 WebAssembly
为了能够正确的编译我们的代码,首先我们需要配置
Cargo.toml
。打开这个文件,将内容改为如下所示:
Cargo.toml
1 | [package] |
你需要改为自己的仓库,同时 Cargo 需要通过 git
来完善
authors
部分。
最重要的是添加底下的部分。第一个部分 — [lib]
— 告诉 Rust
为我们的包建立一个 cdylib
版本;在本教程中我们不会讲解它的含义。有关更多信息,请参阅 Cargo 和 Rust Linkage
文档。
第二个部分是 [dependencies]
部分。在这里我们告诉 Cargo
我们需要依赖哪个版本的 wasm-bindgen
;在这个例子中,它是
0.2.z
版本的 (不是 0.3.0
或者其他版本)。
构建包
现在我们已经完成了所有配置项,开始构建吧!在命令行输入以下命令:
1 | wasm-pack build --scope mynpmusername |
或者
1 | wasm-pack build |
上面的方式会附带你的签名。
这个命令将做一系列事情 (这会花一些时间,特别是当你第一次运行
wasm-pack
)。想了解详细情况,查看这篇在 Mozilla
Hacks 上的文章。简单来说,wasm-pack build
将做以下几件事:
- 将你的 Rust 代码编译成 WebAssembly。
- 在编译好的 WebAssembly 代码基础上运行
wasm-bindgen
,生成一个 JavaScript 文件将 WebAssembly 文件包装成一个模块以便 npm 能够识别它。 - 创建一个
pkg
文件夹并将 JavaScript 文件和生成的 WebAssembly 代码移到其中。 - 读取你的
Cargo.toml
并生成相应的package.json
。 - 复制你的
README.md
(如果有的话) 到文件夹中。
最后的结果?你在 pkg
文件夹下有了一个 npm 包。
报错了?对咯!
教程的说明就截止到上面了,但是大概率,你报错了。
MDN的文章毕竟面对的是全球的开发者,但是在国内有一个比较烦的问题就是网络的访问,在执行wasm-pack build
的时候,它会这样报错:
1 | E:\_Project\Rust\hello-wasm>wasm-pack build |
这里它说是无法下载 wasm-opt 这个东西,但是实际上我直接通过链接是能够直接下载的,网上说更改代理能够解决问题,我这里没有尝试这种方式,但是我还是将这种方法贴在这里:
如果你尝试通过cargo 去提前下载好 wasm-opt
的话,就会遇到另外一个问题,它一会给你报错无法找到c++编译器,但是实际上我的电脑里面安装了vs2019,所以说实际上根本不应该存在这个问题。当我将vs2019的cl.exe添加到环境变量之后,它又会报错说无法找到clang,之后我又去llvm的官网下载了clang,但是它又报错说c++ 'algorithm' file not found
,这个问题我解决不了,暂时也不知道是为什么,这让我浪费了很多时间。
对于新学者来说,最好的办法就是按照它的提示的后者,将wasm-opt = false
添加到你的Cargo.toml
文件中,这样就可以跳过这个步骤,直接编译你的WebAssembly模块。wasm-opt
是一个优化工具,它会对你的WebAssembly模块进行优化,但是实际上,这个优化工具并不是必须的,你可以直接跳过这个步骤,直接编译你的WebAssembly模块。
对了,还有一个问题,就是它虽然叫你需要在 package metadata
中添加wasm-opt = false
,但是实际上,你并不知道在哪里添加,需要翻阅官方文档:Cargo.toml
Configuration - Hello wasm-pack!
wasm-pack
可以通过Cargo.toml
中的package.metadata.wasm-pack
键进行配置。 每个选项都有一个默认值,不是必需的。共有三个配置文件:
dev
、profiling
和release
。 这些对应于传递给wasm-pack build
的--dev
、--profiling
和--release
标志。可用的配置选项及其默认值如下所示:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36 [package.metadata.wasm-pack.profile.dev]
# Should `wasm-opt` be used to further optimize the wasm binary generated after
# the Rust compiler has finished? Using `wasm-opt` can often further decrease
# binary size or do clever tricks that haven't made their way into LLVM yet.
#
# Configuration is set to `false` by default for the dev profile, but it can
# be set to an array of strings which are explicit arguments to pass to
# `wasm-opt`. For example `['-Os']` would optimize for size while `['-O4']`
# would execute very expensive optimizations passes
wasm-opt = ['-O']
[package.metadata.wasm-pack.profile.dev.wasm-bindgen]
# Should we enable wasm-bindgen's debug assertions in its generated JS glue?
debug-js-glue = true
# Should wasm-bindgen demangle the symbols in the "name" custom section?
demangle-name-section = true
# Should we emit the DWARF debug info custom sections?
dwarf-debug-info = false
[package.metadata.wasm-pack.profile.profiling]
wasm-opt = ['-O']
[package.metadata.wasm-pack.profile.profiling.wasm-bindgen]
debug-js-glue = false
demangle-name-section = true
dwarf-debug-info = false
# `wasm-opt` is on by default in for the release profile, but it can be
# disabled by setting it to `false`
[package.metadata.wasm-pack.profile.release]
wasm-opt = false
[package.metadata.wasm-pack.profile.release.wasm-bindgen]
debug-js-glue = false
demangle-name-section = true
dwarf-debug-info = false
通过翻阅文档,我们知道,我们需要在package.metadata.wasm-pack.profile.release
中添加wasm-opt = false
,这样就可以跳过这个步骤了。
修改后的Cargo.toml
文件如下:
1 | [package] |
对代码体积的一些说明
如果你检查生成的 WebAssembly 文件体积,它可能有几百 kB。我们没有让 Rust 去压缩生成的代码,从而大大减少生成包的体积。这和本次教程主题无关,但如果你想了解更多,查看 Rust WebAssembly 工作组文档上关于 减少 .wasm 体积 的说明。
把我们的包发布到 npm
把我们的新包发布到 npm registry:
1 | cd pkg |
我们现在有了一个 npm 包,使用 Rust 编写,但已经被编译为 WebAssembly 了。 现在这个包已经可以被 JavaScript 使用了,而且使用它完全不需要用户安装 Rust;包中的代码是 WebAssembly 代码,而不是 Rust 源码!
在网站上使用我们的包
让我们建立一个使用我们包的网站!人们通过各种打包工具使用 npm
包,在本教程中,我们将使用
vite
。它比其他某些打包工具要更为的便捷,但是你可以使用任何你喜欢的工具。
从这里开始,将会和MDN的教程有所不同,因为我没有使用webpack,而是使用vite,所以说这里将会给出使用vite的教程。
让我们离开pkg
目录,并创建一个新目录site
,尝试以下操作:
1 | cd ../.. |
创建一个新文件 package.json
,然后输入如下代码:
1 | { |
如果上面你并没有将自己的包发布到npm上,那么你需要手动将上一步生成的pkg
文件夹拷贝到site
文件夹下,然后在package.json
中将dependencies
中的@mynpmusername/hello-wasm
改为file:./pkg
。
这样它将会从本地的pkg
文件夹中引入你的WebAssembly模块。
如果上面你将自己的包发布到npm上了,那么请注意,你需要在依赖项部分的
@
之后填写自己的用户名。
接下来,我们需要配置 Vite 来使用 WebAssembly。创建一个新文件
vite.config.js
,并输入如下内容:
1 | import { defineConfig } from 'vite' |
这里的optimizeDeps
是为了解决一个问题,就是vite会将你的WebAssembly模块当作一个模块来处理,但是实际上它并不是一个模块,所以说我们需要将它排除在外,防止vite对它进行处理,使得模块找不到对应的文件。
vite-plugin-wasm 是一个 Vite 插件,它会帮助你加载 WebAssembly 模块。vite-plugin-top-level-await 是一个 Vite 插件,它会帮助你使用顶层 await。
接下来,我们需要一个 HTML
文件。创建一个index.html
并写入如下内容:
1 |
|
需要注意的是,MDN
里面使用的是<script src="./index.js"></script>
,但是实际上,vite是不支持这种方式的,你需要使用<script type="module" src="./index.js"></script>
这种方式来引入你的js文件。
这样的话,你可以在index.js
中直接使用import {greet} from '@mynpmusername/hello-wasm'
来引入你的WebAssembly模块,而不需要额外的处理。
现在我们来写index.js
文件:
1 | import * as wasm from "hello-wasm"; |
加载后,它将从该模块调用greet
函数,并传入字符串“WebAssembly”参数。注意这里看上去没有什么特别的,但是我们正在调用
Rust 代码!就 JavaScript 代码所知,这只是一个普通模块。
我们已经完成了所有的文件!让我们试一下:
1 | npm install |
这将启动一个 Web 服务器。访问 vite
给出的链接,比如,http://localhost:5178/
你应该会在屏幕上看到一个内容为 Hello, WebAssembly!
的警告框。点击按钮时也会看到这个警告框。
我们已经成功地从 JavaScript 调用了 Rust,并从 Rust 调用了 JavaScript。
结论
本教程到此结束。希望你觉得它有用。
非常感谢来自 Rust 和 WebAssembly 社区的所有人,他们的工作使得这一切成为可能。 同样也非常感谢这位作者,他的文章非常通俗易懂,且提供了github上的代码仓库,让我们可以直接下载代码进行测试: 我使用 vue3 + WebAssembly 做了个文件校验网站,性能提升600%_vue webassembly-CSDN博客 jethroHuang/fast_hash
它的这里面还使用了worker来进一步提高性能,这里就不再赘述了,有兴趣的可以去看看。关于worker的内容,可以参考这篇文章:Web Workers API - Web APIs | MDN