背景:项目中需在前端计算文件的Md5值,使用js计算时耗时较长,并且对于超大的文件会溢出错误。
一、WebAssembly简介和使用(以C为例)
WebAssembly 是一种运行在现代 web 浏览器中的新型代码,它是通过虚拟机的方式,可以在服务端、客户端如浏览器等环境执行的二进制程序。他有速度快、效率高、可移植的特点并且提供新的性能特性,同时提升了性能。主要有以下优点:
- 快速、高效、可移植——通过利用常见的硬件能力,WebAssembly 代码在不同平台上能够以接近本地速度运行。
- 可读、可调试——WebAssembly 是一门低阶语言,但是它有确实有一种人类可读的文本格式(其标准即将得到最终版本),这允许通过手工来写代码,看代码以及调试代码。
- 保持安全——WebAssembly 被限制运行在一个安全的沙箱执行环境中。像其他网络代码一样,它遵循浏览器的同源策略和授权策略。
- 不破坏 Web——WebAssembly 的设计原则是与其他网络技术和谐共处并保持向后兼容。
注:wasm 并不是传统意义上汇编语言(Assembly),而是一种中间编译的字节码,可以在浏览器上运行非 JavaScript 语言,只要它能被编译成 wasm。
目前支持wasm的语言有
-
C/C++
- 编译工具:Emscripten
- 说明:Emscripten使用LLVM后端将C和C++代码编译为Wasm,并提供了丰富的标准库支持。
-
Rust
- 编译工具:rustc(Rust编译器)
- 说明:Rust具有良好的Wasam支持,通过
wasm32-unknown-unknown目标可以直接生成Wasm二进制文件。此外,还有wasm-bindgen和wasm-pack等工具帮助与JavaScript交互。
-
Go
- 编译工具:Go编译器(从Go 1.11开始内置支持)
- 说明:通过设置环境变量
GOOS=js和GOARCH=wasm,可以将Go代码编译为Wasm格式。
-
AssemblyScript
- 编译工具:AssemblyScript编译器
- 说明:AssemblyScript是一种类似于TypeScript的语言,专门用于生成高效的Wasm代码。
-
Zig
- 编译工具:Zig编译器
- 说明:Zig是一种低级编程语言,可以通过其编译器直接生成Wasm代码。
-
Kotlin/Native
- 编译工具:Kotlin/Native编译器
- 说明:Kotlin/Native支持生成Wasm目标,通过配置Gradle插件可以编译Kotlin代码到Wasm。
-
Blazor (C#)
- 编译工具:.NET SDK
- 说明:Blazor是一个基于.NET的框架,允许使用C#进行前端开发,并生成Wasm应用程序。
-
Python
- 编译工具:Pyodide, PyScript
- 说明:Pyodide项目将Python解释器移植到了Wasm,使得Python代码可以在浏览器中运行。
-
Java
- 编译工具:TeaVM, JWebAssembly
- 说明:TeaVM和JWebAssembly都是将Java字节码转换为Wasm的工具。
-
Swift
- 编译工具:SwiftWasm
- 说明:SwiftWasm是一个社区驱动的项目,将Swift编译到Wasm。
1、安装编译器工具Emscripten
Emscripten is a toolchain for compiling to asm.js and WebAssembly, built using LLVM, that lets you run C and C++ on the web at near-native speed without plugins.
Emscripten 是一个编译器工具链,使用 LLVM 去编译出 wasm
可在命令窗口按下列步骤完成安装
拉取仓库
git clone https://github.com/emscripten-core/emsdk.git
# 进入目录
cd emsdk
# 下载最新 SDK 工具
./emsdk install latest
# 版本设置为最新
./emsdk activate latest
# 将相关命令行工具加入到 PATH 环境变量中(临时)
source ./emsdk_env.sh
# 检查是否安装成功
emcc -v
2、编译c代码
(1)新建文件(hello.c)
#include <stdio.h>;
int main(int argc, char ** argv) {
printf("Hello World\n");
}
(2)执行emcc hello.c会生成js和wasm文件
js 文件是胶水代码,用来加载和执行wasm的,wasm不能直接作为入口文件使用
(3)使用wasm
-
在node中使用,执行
node a.out.js效果如下: -
在HTML中使用
可以使用Emscripten自带工具,执行以下👇🏻命令生成页面
emcc hello.cpp -o hello.html
需启动serve, 访问页面:
当然也可以直接在html中引入js文件,调用对应方法。
二、Rust
Rust以高性能和内存安全性著称,大家或多或少都有见到过,,近年来在很多个地方得到了广泛应用。现在很多前端构建工具也使用了Rust来提高效率,如 SWC 和 Parcel 利用Rust实现了快速的JavaScript/TypeScript编译和打包功能,故考虑使用rust实现;
1、安装Rust
参考安装文档: doc.rust-lang.org/book/ch01-0…
2、Cargo
Cargo是rust的构建系统和包管理器,大多数的rust开发者使用它来管理rust项目。Cargo能够为你处理大多数的任务,i.g. 构建代码,下载依赖包,包构建 .etc 。上一步安装rust的同时也安装了cargo,后面我们可以直接使用, 以下为cargo使用示例,可以用于学习rust;
创建一个rust工程
$ cargo new hello_cargo
$ cd hello_cargo
构建项目
$ cargo build
运行项目(编译+运行)
$ cargo run
cargo搭建的项目使用可参考rust文档;
3、安装cargo-generate
cargo-generate时rust根据模板生成项目的工具,可直接使用cargo安装
注意:M1 芯片 安装 cargo-generate 需要带参数--features "vendored-openssl" 😤
执行以下命令安装cargo-generate
cargo install cargo-generate
4、编写Hello Rust
fn main () {
println!("Hello, Rust!");
}
- fn表示定义一个函数;
- main函数为Rust程序的的入口;
在命令窗口执行下面命令编译rust为可执行文件
rustc hello.rs
运行编译好的文件效果如下:
自此我们已经了解了rust的基本使用🐶;
三、使用wasm-pack构建项目
wasm-pack是构建和使用 rust 生成的 WebAssembly 的一站式工具
1、初始化项目
使用cargo初始化一个项目:wasm-pack-template是初始化项目使用的模板
cargo generate --git https://github.com/rustwasm/wasm-pack-template
执行完成后根据命令提示输入项目名称,即可生成rust-wasm项目;
2、目录结构
完成上一步操作后会生成以下目录结构的一个wasm项目:
wasm
├── Cargo.toml
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
└── src
├── lib.rs
└── utils.rs
- Cargo.toml为 项目清单文件,包含元数据,例如包的名称、版本和依赖,(作用类似前端项目的package.js)在 Rust 中称为“crates”;
- src/lib.ts 是模板的主要源文件。 也是我们导出方法的入口文件,lib表示 Rust 项目将被编译为一个库;
- src/utils.rs 是定义utils方法的地方。可以将一些公共方法写到这里;
3、打包项目
模板中有默认的一个alert调用代码,我们可以直接打包测试;
执行命令打包项目:
wasm-pack build
构建成功后,项目目录下会生成一个pkg的文件目录,这就是打包完成的wasm项目
pkg/
├── package.json
├── README.md
├── wasm_game_of_life_bg.wasm
├── wasm_game_of_life.d.ts
└── wasm_game_of_life.js
- .wasm文件是Rust编译器从Rust源代码生成的WebAssembly二进制文件。它包含我们所有Rust函数和数据的编译到wasm版本。
- .js文件由wasm-bindgen生成,包含JavaScript链接wasm,用于将DOM和JavaScript函数导入Rust,并将WebAssembly函数的API暴露给JavaScript。
- .d.ts文件包含JavaScrip的ts类型申明
4、本地调试
执行👇🏻命令, 初始化www目录
npm init wasm-app www
执行上一步后,会生成一个www目录,可以作为我们本地调试开发的web项目;
(1)package.json中添加本地包的引用(地址指向本地文件路径即可
(2)修改index.js代码调用wasm方法
- 引入wasm
- 调用wasm方法
(3)启动www项目调试
在www目录下执行
npm run start
四、编写rust代码实现MD5计算
MD5(Message Digest Algorithm 5)是一种广泛使用的哈希函数,用于生成数据的128位(16字节)散列值。 下面用rust开发md5方法,这里我们直接调用rust的md5包来实现
首先执行命令 cargo add md5 在项目中安装md5包,在Cargo.toml文件中可以看到完成安装的依赖
下面来编写实现代码,
在默认lib.rs文件中,顶部有一行代码需要我们保留;
use wasm_bindgen::prelude::*;
它表示引入wasm_bindgen库,作用是实现 Rust 和 WebAssembly 之间进行交互的库,prelude为wasm_bindgen下的模块, * 表示引入全部模块
接着添加以下代码
use md5;
#[wasm_bindgen]
pub fn bcc_wasm_md5(input: &[u8]) -> String {
let digest = md5::compute(input);
format!("{:x}", digest)
}
-
use md5表示引入md5包;
-
#[wasm_bindgen]是一个属性宏,用于将 Rust 代码导出为 WebAssembly,并使其能够与 JavaScript 进行交互;
-
bcc_wasm_md5为定义的对外暴露的方法名,pub 修饰符用于将项设为公有,使其可以在模块外部访问,从而实现模块之间的代码共享和复用;
-
&[u8] 表示引用的u8类型的切片,用于定义入参类型
-
rust默认最后值没用;结尾为函数返回值, 故可以不写return
同理:如果想把浏览器的api在wasm中调用如何实现呢?
如果,想在wasm中调用浏览器方法alert,可以这样导入到wasm;
#[wasm_bindgen]
extern "C" {
fn alert(s: &str);
}
包发布
代码编写完成后我们可以将wasm发布成为一个npm包
wasm-pack publish
五、性能分析
时长计算代码如下:
测试结果如下:
| 文件大小 | js(CryptoJS)时长 | wasm(rust)时长 |
|---|---|---|
| 20 KB(jpg) | 7.928955078125 ms | 3.02001953125 m |
| 2.2MB(mp4) | 14.693115234375 ms | 4.7900390625 ms |
| 5.1MB(mp4) | 178.071044921875 ms | 37.09521484375 ms |
| 15.1MB(mp4) | 479.489990234375 ms | 87.883056640625 ms |
| 19.1MB(pdf) | 586.2041015625 ms | 105.72509765625 ms |
| 1.09GB(mp4) | 溢出报错 | 5644.2548828125 ms |
综上测试结果: 文件越大,提升效率也越明显; wasm耗时为js的 18% ~ 38% , 效率提升 60% ~ 80%;效率提升显著。
六、使用和展望
1、在项目中使用wasm包
安装npm包
$ npm install xxxxx
配置项目对wasm的支持,以vite为例,安装包,配置vite.config
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
export default defineConfig({
plugins: [
wasm(),
topLevelAwait()
]
});
js代码中使用:
import { bcc_wasm_md5 } from "xxxx";
const calculateBtn = () => {
const fileInput = document.getElementById('fileInput') as HTMLInputElement;
const file = fileInput.files?.[0];
if (!file) {
alert('Please select a file');
return;
}
const reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = () => {
const arrayBuffer = reader.result as ArrayBuffer;
const uint8Array = new Uint8Array(arrayBuffer);
const hash = bcc_wasm_md5(uint8Array);
console.log(hash);
}
}
2、wasm的应用场景
wasm,并于2019年12月5日正式成为W3C recommendation,但在实际项目中使用的频率非常低,目前只看到在音视频网站、游戏和类似figma这类图片编辑的的项目中看到有使用,究其原因应该还是大多数前端项目对计算性能的要求并不多;
wasm的另一个应用领域是代码可可移植性,很多非前端项目想要移植到web平台,也可以考虑该方案