Rust 学习笔记 - WebAssembly 初探

763 阅读5分钟

基本概念

WebAssembly 是一种新的编码方式,可以在现代浏览器中运行。

  • 它是一种低级的类汇编语言

  • 具有紧凑的二进制格式

  • 可以接近原生的性能运行

  • 并为 C/C++、C#、Rust 等语言提供一个编译目标,以便它们可以在 Web 上运行

  • 它也被设计为可以与 JavaScript 共存,允许两者一起工作

理解机器码、汇编语言与 WebAssembly

机器码是计算机可以直接执行的语言,而汇编语言比较接近机器码,不过也需要转换成机器码之后计算机才可以执行。

不同的 CPU 架构(ARM、x64、x86)需要不同的机器码和汇编,高级语言(C/C++、C# 等)可以编译成机器码,以便在 CPU 上运行。

而 WebAssembly 其实并不是汇编语言,它不只对特定的机器,而是针对浏览器的。WebAssembly 是中间编译器目标。

文件格式

  • 文本格式:.wat (TEXTUAL FORMAT)

  • 二进制格式:.wasm (BINARY FORMAT)

WebAssembly 能做什么

  • 可以把你编写的 C/C++、C#、Rust 等语言的代码编译成 WebAssembly 模块

  • 你可以在 Web 应用中加载该模块,并通过 JavaScript 调用它

  • 它并不是为了替代 JS,而是与 JS 一起工作

  • 仍然需要 HTML 和 JS,因为 WebAssembly 无法访问平台 API,例如 DOM、WebGL 等

WebAssembly 如何工作

以 C/C++ 为例子:

hello.c -> EMSCRIPTEN -> hello.wasm

C/C++ 代码通过 EMSCRIPTEN 编译器编译为 .wasm 文件,然后配合 js 和 HTML 文件一起在浏览器中工作。

WebAssembly 优点

  • 快速、高效、可移植:通过利用常见的硬件能力,WebAssembly 代码在不同平台上能够以接近本地速度运行

  • 可读、可调式:WebAssembly 是一门低阶语言,但是它却有一种人类可读的文本格式(其标准最终版本目前仍在编制),这允许通过手工来写代码,看代码以及调试代码

  • 保持安全:WebAssembly 被限制运行在一个安全的沙箱执行环境中。像其它网络代码一样,它遵循浏览器的同源策略和授权策略

  • 不破坏网络:WebAssembly 的设计原则是与其它网络技术和谐共处并保持向后兼容

环境搭建

官方文档:

Introduction - Rust and WebAssembly

Setup - Rust and WebAssembly

Step1 安装 wasm-pack

下载地址 wasm-pack

按照官方指引执行:

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

执行后就直接安装了,如果网络不好可能会安装失败,多试几次,成功之后会有类似下面的提示:

info: downloading wasm-pack
info: successfully installed wasm-pack to `/Users/eagleye/.cargo/bin/wasm-pack`

Step2 安装 cargo-generate

cargo install cargo-generate

这个也可能会遇到网络问题,多试几次就好。

Step3 安装 npm

安装 npm 其实就是安装 NodeJS

官网地址:Node.js (nodejs.org)

下载安装好之后,验证一下是否安装好,执行:

npm --version

创建 Hello, World 程序

Step1 WebAssembly 程序准备

克隆官方的模板工程:

cargo generate --git https://github.com/rustwasm/wasm-pack-template

如果网不好可能也会出现失败,多试几次,如果克隆成功会提示输入一个项目名称,官方给的是 wasm-game-of-life

完成之后进入到这个目录中:

cd wasm-game-of-life

目录结构如下图所示:

wasm2.jpg

里面有个 Cargo.toml 文件,其中 wasm-bindgen 是一个比较关键的依赖,就是依赖它与 JavaScript 接口的。

再看 src/lib.rs 文件:

mod utils;

use wasm_bindgen::prelude::*;

// When the `wee_alloc` feature is enabled, use `wee_alloc` as the global
// allocator.
#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;

#[wasm_bindgen]
extern {
    fn alert(s: &str); // 此处就是 js 的 window.alert 函数的声明
    // 声明了才能在 rust 中使用
}

#[wasm_bindgen]
pub fn greet() { // greet 函数加了 #[wasm_bindgen],所以它是能被前端调用的
    // 此处是 rust 代码,调用的就是上面声明的 alert
    alert("Hello, wasm-game-of-life!");
}

如果没有 wasm32-unknown-unknown 那么需要先进行手动安装才能编译项目:

rustup target add wasm32-unknown-unknown

编译项目(这里有个坑如果是新装的 wasm32-unknown-unknown 记得重启一下命令窗):

wasm-pack build

第一次编译可能比较慢,因为需要下载 wasm_bindgen 依赖。

编好之后的文件都在 pkg 文件夹中。

Step2 前端程序准备

快速地生成一个套前端代码:

npm init wasm-app www

看到这个输出就是生成好了 🦀 Rust + 🕸 Wasm = ❤

这样就会生成一个 www 文件夹,这里面就是我们的前端代码。

目录结构如下所示:

wasm3.jpg

然后,配置前端程序的依赖项去依赖我们打包出来的 WebAssembly 的程序。

配置 www/package.json,追加依赖项配置:

"dependencies": {
    "wasm-game-of-life": "file:../pkg"
 }

wasm-game-of-life 这个名字要与 pkg/package.json 中的 name 保持一致。

最后切到 www 目录下安装依赖:

cd www

npm install

Step3 修改前端代码

www/index.js

import * as wasm from "wasm-game-of-life"; // 引用我们自己编的 wasm 项目

wasm.greet(); // 调用 greet 函数

运行:

npm start

访问:http://localhost:8080

访问效果如下图所示:

wasm1.jpg

性能对比

我将用三种方式实现一个爬楼梯算法,当然用的是最耗时的一种算法,最朴素的递归计算。题目原文: 70. 爬楼梯 - 力扣(LeetCode) (leetcode-cn.com)

以下结果都是在同一台电脑上运行得出的数据,不同的系统,不同的电脑测试数据肯定也会不一样,所以数据只作为参考。

JavaScript 实现

const climbStairs = function (n) {
  if (n <= 2) {
    return n;
  }
  return climbStairs(n - 1) + climbStairs(n - 2);
};
console.time();
console.log(climbStairs(42));
console.timeEnd();

测试几轮下来,耗时基本在 2600 ~ 3050 ms 左右。

WebAssembly 实现

#[wasm_bindgen]
pub fn climb_stairs(n: i32) -> i32 {
    if n <= 2 {
        return n
    }
    return climb_stairs(n - 1) + climb_stairs(n - 2);
}
import * as wasm from "wasm-game-of-life";

console.time();
console.log(wasm.climb_stairs(42));
console.timeEnd()

测试几轮下来,耗时基本在 1600 ~ 1900 ms 左右。

Rust 实现

use std::time::Instant;
fn main() {
    let now = Instant::now();
    climb_stairs(42);
    let duration = now.elapsed();
    println!("{}", duration.as_micros());
}

pub fn climb_stairs(n: i32) -> i32 {
    if n <= 2 {
        return n
    }
    return climb_stairs(n - 1) + climb_stairs(n - 2);
}

测试几轮下来,直接 cargo run 运行耗时基本在 2100 ~ 2400 ms 左右。但是如果 cargo build --release 编译之后运行,运行耗时基本在 700 ~ 800 ms 左右。

综上所述,WebAssembly 的性能肯定是不如原生 rust 的,但是比 JavaScript 还是要好很多的。