Rust + WebAssembly加速文件MD5计算

814 阅读9分钟

背景:项目中需在前端计算文件的Md5值,使用js计算时耗时较长,并且对于超大的文件会溢出错误。

一、WebAssembly简介和使用(以C为例)

WebAssembly 是一种运行在现代 web 浏览器中的新型代码,它是通过虚拟机的方式,可以在服务端、客户端如浏览器等环境执行的二进制程序。他有速度快、效率高、可移植的特点并且提供新的性能特性,同时提升了性能。主要有以下优点:

  • 快速、高效、可移植——通过利用常见的硬件能力,WebAssembly 代码在不同平台上能够以接近本地速度运行。
  • 可读、可调试——WebAssembly 是一门低阶语言,但是它有确实有一种人类可读的文本格式(其标准即将得到最终版本),这允许通过手工来写代码,看代码以及调试代码。
  • 保持安全——WebAssembly 被限制运行在一个安全的沙箱执行环境中。像其他网络代码一样,它遵循浏览器的同源策略和授权策略。
  • 不破坏 Web——WebAssembly 的设计原则是与其他网络技术和谐共处并保持向后兼容。

注:wasm 并不是传统意义上汇编语言(Assembly),而是一种中间编译的字节码,可以在浏览器上运行非 JavaScript 语言,只要它能被编译成 wasm。

目前支持wasm的语言有

  1. C/C++

    • 编译工具:Emscripten
    • 说明:Emscripten使用LLVM后端将C和C++代码编译为Wasm,并提供了丰富的标准库支持。
  2. Rust

    • 编译工具:rustc(Rust编译器)
    • 说明:Rust具有良好的Wasam支持,通过wasm32-unknown-unknown目标可以直接生成Wasm二进制文件。此外,还有wasm-bindgenwasm-pack等工具帮助与JavaScript交互。
  3. Go

    • 编译工具:Go编译器(从Go 1.11开始内置支持)
    • 说明:通过设置环境变量GOOS=jsGOARCH=wasm,可以将Go代码编译为Wasm格式。
  4. AssemblyScript

    • 编译工具:AssemblyScript编译器
    • 说明:AssemblyScript是一种类似于TypeScript的语言,专门用于生成高效的Wasm代码。
  5. Zig

    • 编译工具:Zig编译器
    • 说明:Zig是一种低级编程语言,可以通过其编译器直接生成Wasm代码。
  6. Kotlin/Native

    • 编译工具:Kotlin/Native编译器
    • 说明:Kotlin/Native支持生成Wasm目标,通过配置Gradle插件可以编译Kotlin代码到Wasm。
  7. Blazor (C#)

    • 编译工具:.NET SDK
    • 说明:Blazor是一个基于.NET的框架,允许使用C#进行前端开发,并生成Wasm应用程序。
  8. Python

    • 编译工具:Pyodide, PyScript
    • 说明:Pyodide项目将Python解释器移植到了Wasm,使得Python代码可以在浏览器中运行。
  9. Java

    • 编译工具:TeaVM, JWebAssembly
    • 说明:TeaVM和JWebAssembly都是将Java字节码转换为Wasm的工具。
  10. 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文件

us7uv71vg4.png

js 文件是胶水代码,用来加载和执行wasm的,wasm不能直接作为入口文件使用

(3)使用wasm

  • 在node中使用,执行node a.out.js效果如下: 34egxw3lz61.png

  • 在HTML中使用

可以使用Emscripten自带工具,执行以下👇🏻命令生成页面

emcc hello.cpp -o hello.html

需启动serve, 访问页面:

mqxpiyqdwh1.png

当然也可以直接在html中引入js文件,调用对应方法。

二、Rust

Rust以高性能和内存安全性著称,大家或多或少都有见到过,,近年来在很多个地方得到了广泛应用。现在很多前端构建工具也使用了Rust来提高效率,如 SWC 和 Parcel 利用Rust实现了快速的JavaScript/TypeScript编译和打包功能,故考虑使用rust实现;

文档:doc.rust-lang.org/book/title-…

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

运行编译好的文件效果如下: vii441khul1.png

自此我们已经了解了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中添加本地包的引用(地址指向本地文件路径即可

c43312f5eb024b019820ff9b9479ed87.png

(2)修改index.js代码调用wasm方法

  • 引入wasm

fz7mo71fqr.png

  • 调用wasm方法 image.png

(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文件中可以看到完成安装的依赖 ce08h6s358.png

下面来编写实现代码,

在默认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

五、性能分析

时长计算代码如下: jdcsympmx.png hzma3r9ww.png

测试结果如下:

文件大小js(CryptoJS)时长wasm(rust)时长
20 KB(jpg)7.928955078125 ms3.02001953125 m
2.2MB(mp4)14.693115234375 ms4.7900390625 ms
5.1MB(mp4)178.071044921875 ms37.09521484375 ms
15.1MB(mp4)479.489990234375 ms87.883056640625 ms
19.1MB(pdf)586.2041015625 ms105.72509765625 ms
1.09GB(mp4)溢出报错5644.2548828125 ms

p32oewbvp1.png image.png jv9uvr8kdg1.png oh50y6xsai.png a08741db1cec41a284c82f953d6f27c9.png IsU2Qq4L0S91gHp6s.png

综上测试结果: 文件越大,提升效率也越明显; 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平台,也可以考虑该方案