前言
之前在一个自己的项目中尝试做一个web
视频转码功能,计划用的是ffmpeg
这个强大的库。当时就了解到了wasm
把ffmpeg
移植到浏览器中使用。但是等真正要发布到生产的时候还是遇到一些问题,
比如说ffmpeg
体积比较大,加载速度缓慢;还有sharedArrayBuffer
与ffmpeg.wasm
的一些关系,简单来说就是如果需要使用多线程版的ffmpeg
,就需要设置COOP/COEP
这两个新的跨域策略,但是设置这两个东西就会破坏OAuth
的集成;或者可以选择使用单线程的ffmpeg
,但是效率感人。。
但是这并不影响将C/C++/Rust
等语言编译成Wasm
移植到浏览器依旧是一种很有魅力的解决方案,今天一起来走进它吧!
Wasm简介
WebAssembly(Wasm)
是一种开放标准,旨在提供一种可移植、高性能的二进制格式,用于在web
浏览器中运行。它不是特定于任何语言的,而是为多种编程语言设计,包括C、C++、Rust
等。通过将代码编译为Wasm
格式,开发人员可以实现在不同平台和浏览器上运行的一致性性能。
Wasm
的主要目标之一是提供比传统的JavaScript
更高效的执行速度。它允许开发人员使用其他语言编写部分应用程序,然后将这些部分集成到web
应用程序中,实现更好的性能和更广泛的语言选择。
此外,Wasm
还提供了安全性、可移植性和版本控制等方面的优势。它在web
浏览器中作为一个虚拟机执行,与浏览器的JavaScript
引擎紧密集成,使得web
应用程序可以更高效地利用底层硬件资源。
Hello Rust
Rust
是一种系统级编程语言,注重内存安全、并发性和性能。由Mozilla
开发,使用它可以高效地控制硬件,同时保持高级语言的安全性。具体有以下比较突出的特点:
- 内存安全 :
Rust
通过所有权系统、生命周期检查和借用机制,有效地防止了空指针引用、数据竞争和内存泄漏等内存安全问题,使得编写安全的并发代码更为容易。 - 性能 :
Rust
提供了接近底层语言(如C
和C++
)的性能,同时保持了高级语言的抽象特性。零成本抽象的设计意味着你可以高效地控制硬件,而不会损失性能。 - 并发性 :
Rust
通过所有权系统和借用机制,支持并发编程,同时避免了常见的并发错误。这使得开发者能够编写线程安全的代码,而不需要额外的锁或同步原语。 - 生态系统 :
Rust
拥有一个不断壮大的生态系统,有丰富的库和工具,涵盖了各种应用场景。这使得开发者能够更容易地构建各种类型的应用,从系统级应用到Web
服务。 - 开发者友好 :
Rust
的语法清晰、现代化,拥有友好的文档和社区支持。它鼓励编写易读易维护的代码,同时提供了丰富的工具链和调试支持。
Rust
的具体安装方式可以参考这个文档:Rust安装,如果你是Mac
用户,看到下图的时候表示Rust已经安装完毕:
安装Rust
的时候一般情况下会自带安装cargo,它是Rust
的库管理工具,类似于npm
。我们可以使用 cargo new hello_rust
来创建一个Rust
项目。
项目安装好之后结构目录大致如上,如果你是使用vscode
进行开发的话,建议安装rust-analyzer
这个插件,它提供了代码的语法分析、自动完成、错误分析等功能,可以大大的提升我们的开发效率。
下面执行一下cargo run
命令,就可以把我们的Rust
项目跑起来:
计算文件MD5
首先使用cargo new
来创建一个Rust
项目,在Cargo.toml
中填入以下的内容
ini
[package]
name = "rust_md5"
version = "0.1.0"
edition = "2021"
[lib]
crate-type = ["cdylib"]
[dependencies]
md-5 = "0.9.0"
js-sys = "0.3.50"
wasm-bindgen = "0.2.73"
[profile.release]
opt-level = 3
lto = true
strip = true
panic = "abort"
解释一下上面的字段:
package
:包的相关信息name
:指定了你的项目的名称。每个 Rust 项目都有一个唯一的名称。version
:指定了你的项目的版本号。这遵循语义化版本规范(Semantic Versioning),通常包括主版本号、次版本号和修订号。edition
:指定了 Rust 编译器所使用的语言版本。在这里,它指定了项目使用 Rust 2021 Edition。
lib
crate-type
:这里指定了生成的 crate 的类型为动态链接库(.cdylib
),这通常用于构建 WebAssembly 模块。
dependencies
依赖项[profile.release]
:关于 release 模式的配置。opt-level = 3
:指定了编译器的优化级别。在 release 模式下,通常选择最高级别(3),以便进行更强大的优化。lto = true
:启用 Link Time Optimization(LTO),这允许在链接阶段进行更广泛的优化。strip = true
:启用在编译结束后去除调试信息和未使用的代码等优化,以减小生成的二进制文件的大小。panic = "abort"
:指定了在 release 模式下发生 panic 时的处理方式。这里设置为 "abort",表示在发生 panic 时立即终止程序。
然后在src
文件夹下新增一个lib.rs
文件,利用rust
的md5
库来计算文件的md5
,其中输入是uint8
数组,输出是一个字符串
rs
use md5::{Digest, Md5};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsValue;
#[wasm_bindgen]
#[repr(C)]
pub struct RustMd5 {
}
#[wasm_bindgen]
impl RustMd5 {
pub fn new() -> Self {
RustMd5 {
}
}
pub fn calculate_md5(&self, file_buffer: &[u8]) -> Result<String, JsValue> {
let mut md5 = Md5::new();
md5.update(file_buffer);
let result = md5.finalize();
let md5_string = result
.iter()
.map(|b| format!("{:02x}", b))
.collect::<String>();
Ok(md5_string)
}
}
打包Wasm
接下来我们就可以把这个rust
工程来打包成wasm
产物,使用到的是wasm-pack
这个工具,首先可以使用cargo install wasm-pack
来安装这个打包工具,然后执行wasm-pack build
就可以开始打包。
打包出来的产物如下
Vite引入使用
打包好wasm
模块之后,我们就可以将其引入到项目中使用了,这里我以vite
搭建的工程为例,介绍如何把wasm
模块引入到项目之中使用。Vite
的配置文件如下,重点需要关注的是vite-plugin-wasm
和vite-plugin-top-level-await
这两个包,记得提前安装好。
js
import { defineConfig } from "vite";
import react from "@vitejs/plugin-react";
import wasm from "vite-plugin-wasm";
import topLevelAwait from "vite-plugin-top-level-await";
export default defineConfig({
plugins: [
react(),
wasm(),
topLevelAwait()
],
});
然后把打包好的pkg
文件夹放到前端项目下,用以下的方式引入:
js
import * as wasm from "./pkg/rust_md5.js";
console.log(wasm);
const rustMd5 = wasm.RustMd5.new();
接下来就可以上传文件并计算文件的MD5
了,这里需要注意的是我们写的Rust
模块计算MD5
的方法接受的是一个Uint8Array
,所以前端需要转换一下再传输给Rust
,示例代码如下:
js
const handleFileChange = (e) => {
const uploadedFile = e.target.files[0];
// 使用 FileReader 读取文件并转换为 ArrayBuffer
const fileReader = new FileReader();
console.log("读取文件并转换为 ArrayBuffer");
fileReader.onload = function (e) {
// 获取 ArrayBuffer
const arrayBuffer = e.target.result;
const startTime = performance.now();
const uint8Array = new Uint8Array(arrayBuffer);
const res = rustMd5.calculate_md5(uint8Array);
console.log("res", res);
const endTime = performance.now();
const executionTime = (endTime - startTime) / 1000; // 单位:秒
alert(executionTime + "s");
};
// 以 ArrayBuffer 格式读取文件
fileReader.readAsArrayBuffer(uploadedFile);
};
// 省略一些代码
<input type="file" onChange={handleFileChange} />
对比JS
我使用了几种规格的文件大小,分别对JS
计算MD5
和Rust
计算MD5
的速度进行了对比,我的测试笔记本是Apple M1
芯片,8G
内存。
结果如下,单位为秒
文件大小 | 300K | 1.5M | 15M | 125M | 2G |
---|---|---|---|---|---|
Rust | 0.0036 | 0.0040 | 0.032 | 0.2635 | 5.28 |
Js | 0.0124 | 0.028 | 0.16 | 1.26 | 21.148 |
从上面可以看出,Rust
无论在任何文件体积下,速度都比JS
快5-20
倍不等,看到这个结果我不禁感慨,Rust
竟恐怖如斯。
最后
本文以计算MD5
为场景,介绍了Rust
打包Wasm
产物并引入到Vite
中使用的一种方式,纯属抛砖引玉。如果你有其他想法,欢迎评论区或私信交流,如果觉得有趣的话,点点关注点点赞吧~