Rust前端WebAssembly所有权内存安全

前端视角解读 Why Rust:从所有权到 WebAssembly

张华挺 (字节前端)··原文链接

前端视角解读 Why Rust:从所有权到 WebAssembly

作者: 张华挺 | 来源: 字节前端 ByteFE | 时间: 2022年5月9日

因为我们需要使用合适的工具解决合适的问题。


一、为什么要学 Rust?

目前 Rust 对 WebAssembly 的支持是最好的。对于前端开发来说,可以将 CPU 密集型的 JavaScript 逻辑用 Rust 重写,再用 WebAssembly 来运行,JavaScript 和 Rust 的结合将会让你获得驾驭一切的力量

前端需要经历的思维转变

转变
编程范式命令式(imperative)函数式(functional)
变量特性可变性(mutable)不可变性(immutable)
类型系统弱类型语言强类型语言
内存管理手工或自动(GC)通过生命周期管理

难度逐级递增

Rust 的过人之处

  • 内存安全和高性能二者兼得 - 无需 GC 也能保证内存安全
  • 表达力和高性能二者兼得 - 像 Python/TypeScript 一样表达,性能不输 C/C++
  • 编译通过,即可上线 - 友好的编译器和清晰的错误提示

二、堆和栈:内存管理基础

栈空间:LIFO 后进先出

  • 数据存储时只能从顶部逐个存入
  • 每次调用函数,都会在栈顶创建一个栈帧保存上下文
  • 函数返回后,栈帧被释放
  • 效率高,大小在编译期确定

堆空间:无序键值对

  • 无序的 key-value 存储
  • 程序运行时动态分配内存
  • 可以随心所欲增加/删除变量
  • 效率低,空间利用率随碎片化降低

语言对比

特性JavaScriptRust
原始类型栈内存(地址+内容)默认栈存储
引用类型栈存地址,堆存内容显式使用 Box::new() 存堆
动态数组堆存储数据在堆,胖指针在栈
内存回收GC(标记-清除)所有权系统(编译期检查)

三、所有权系统:掌控值的生死大权

核心规则

  1. 一个值只能被一个变量所拥有(所有者)
  2. 一个值同一时刻只能有一个所有者
  3. 当所有者离开作用域,值被丢弃,内存释放

Move 语义

fn main() {
    let data = vec![10, 42, 9, 8];  // data 拥有堆内存
    let pos = find_pos(data, 42);   // data 被 move 到 find_pos
    // data 在这里已失效!不能再使用
}

fn find_pos(data: Vec<u32>, v: u32) -> Option<usize> {
    // data 现在归 find_pos 所有
    for (pos, item) in data.iter().enumerate() {
        if *item == v { return Some(pos); }
    }
    None
    // data 在这里被释放
}

优势:堆上数据始终只有唯一引用,解决多重引用带来的内存管理难题。

Copy 语义

对于存储在栈上的简单数据(实现 Copy trait):

let x: u32 = 42;  // u32 实现了 Copy
let y = x;        // x 被复制到 y,x 仍然可用
println!("{}", x); // OK!

对比:堆数据用 Move,栈数据用 Copy,既安全又高效。


四、借用(Borrow):不转移所有权的使用

不可变借用

fn main() {
    let data = vec![1, 2, 3, 4];
    
    // 使用 & 借用 data
    println!("sum: {}", sum(&data));  // 借用 data
    println!("data: {:?}", data);     // data 仍然可用
}

fn sum(data: &Vec<u32>) -> u32 {
    data.iter().fold(0, |acc, x| acc + x)
    // data 只是借用,不被释放
}

特点

  • 使用 & 实现借用
  • 不破坏值的单一所有权约束
  • 默认情况下,Rust 的借用都是只读的
  • 借用不能超过值的生命周期

五、生命周期:编译期的借用检查

静态生命周期 vs 动态生命周期

类型生命周期示例
全局/静态变量静态字符串字面量、函数指针
栈/堆变量动态局部变量、动态数组

生命周期标注

// 告诉编译器:返回的引用和 s1、s2 有相同生命周期
fn max<'a>(s1: &'a str, s2: &'a str) -> &'a str {
    if s1 > s2 { s1 } else { s2 }
}

借用检查器在编译期比较作用域,确保所有借用都有效。


六、Rust 与 WebAssembly

为什么选 Rust?

WebAssembly 可以在浏览器中以接近原生性能运行。Rust 是目前对 WebAssembly 支持最好的语言

应用场景

浏览器内

  • VR、图像视频编辑、3D 游戏
  • CAD 等专业工具移植
  • 语言编译器/虚拟机

脱离浏览器

  • 游戏分发服务
  • 服务端执行不可信代码
  • 移动混合原生应用

快速开始

# 安装 Rust
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

# 安装 wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

# 创建项目
cargo new picture-wasm

# 构建
wasm-pack build

Vite 集成

使用 vite-plugin-rsw 插件:

  • 支持 Rust 包文件热更新
  • 监听 src 目录和 Cargo.toml 变更
  • 自动构建

Rust 代码示例(图像转灰度)

#[wasm_bindgen]
pub fn grayscale(_array: &[u8]) -> Result<(), JsValue> {
    let mut img = load_image_from_array(_array);
    img = img.grayscale();
    let base64_str = get_image_as_base64(img);
    append_img(base64_str)
}

React 调用

import init, { grayscale } from "picture-wasm";

useEffect(() => {
    init(); // 必须先初始化
}, []);

const handleFile = (e) => {
    const file = e.target.files[0];
    const reader = new FileReader();
    reader.readAsArrayBuffer(file);
    reader.onload = (res) => {
        const uint8Array = new Uint8Array(res.target.result);
        grayscale(uint8Array); // 调用 Rust 函数
    };
};

七、总结

Rust 的核心价值

维度传统语言Rust
内存安全GC 或手动管理所有权系统(编译期检查)
性能需要取舍零成本抽象
并发容易出错所有权保证线程安全
WebAssembly支持有限原生支持

学习曲线虽陡,但值得

Rust 让前端开发者重新思考:

  • 内存管理 - 没有 GC 也能自动释放
  • 类型系统 - 编译期捕获大多数错误
  • 并发编程 - 所有权天然避免数据竞争

"编译通过,即可上线" —— 这是 Rust 编译器给你的承诺。


参考资料