WebAssembly与Rust实践探索

简介

先来说下在 WebAssembly(后续称WASM) 官网上的介绍,主要有四点:

    1. 高效:WASM 有一套完整的语义,实际上 WASM 是体积小且加载快的二进制格式, 其目标就是充分发挥硬件的能力以达到原生语言的执行效率
    1. 安全:WASM 运行在一个内存安全,沙箱化的执行环境中,甚至可以在现有的 JavaScript 虚拟机中实现。在 Web 环境中 ,WASM 将会严格遵守同源策略以及浏览器安全策略
    1. 开放:WASM 设计了一个非常规整的文本格式用来、调试、测试、实验、优化、学习、教学或者编写程序。可以以这种文本格式在 Web 页面上查看 WASM 模块的源码
    1. 标准:WASM 在 Web 中被设计成无版本、特性可测试、向后兼容的。WASM 可以被 JavaScript 调用,进入 JavaScript 上下文,也可以像 Web API 一样调用浏览器的功能。WASM 不仅可以运行在浏览器上,也可以运行在非 Web 环境下(如 Node.js、Deno、物联网设备等。

发展历史

    1. 2013年:Asm.js 发布,虚幻引擎被编译成 Asm.js 移植到浏览器中,这是 WASM 的前身
    1. 2015年:各大浏览器厂商开始合作开发 WASM,成立 W3C WebAssembly Community Group
    1. 2017年:各大浏览器开始支持 WASM
    1. 2018年:2月发布首个公开的草案
    1. 2019年:WASM 发布 1.0 正式版
    1. 2022年:4月发布了 2.0 的草案

兼容性

可以看到目前的主流浏览器:Chrome、Edge、Safari、Firefox、Opera 都已经支持,Safari 11版本(对应 IOS 11)以上的移动端对于 WASM 的支持也比较好了,如果是低于 IOS 11 以下的系统就需要做逻辑兜底的处理了。所以如果是 B 端的项目,可以放心大胆的去在项目中进行落地,如果是 C 端的项目,可能会有一小部分用户的系统会不支持,这时候可以使用 wasm2js 工具来做代码转换兜底。

在正式去了解 WASM 之前我们先来了解一下 LLVM 👇

LLVM

LLVM 是模块化和可重用的编译器和工具链技术的集合,它是由 C++ 编写的。尽管叫做 LLVM,但它跟传统虚拟机几乎没啥关系。“LLVM” 这个名称本身并不是首字母缩写(并不是 Low Level Virtual Machine),LLVM 就是它的全称。它用于优化以任意的编程语言编写的程序的编译时间、链接时间、运行时间以及空闲时间,经过各种优化后,输出一套适合编译器系统的中间语言,目前采用它来做转换的语言有很多:Swift、Object-C、C#、Rust、Java字节码等。WASM 编译器底层也使用了LLVM 去将原生代码(如Rust、C、C++等)转换成 WASM 二进制代码。

编译器

编译器包括三部分:

    1. 前端:负责处理源语言
    1. 优化器:负责优化代码
    1. 后端:负责处理目标语言

前端

前端在接收到代码的时候就会去解析它,然后检查代码是否有语法或语法问题,然后代码就会转换成中间表示产物(intermediate representation) IR。

优化器

优化器会去分析 IR 并将其转换成更加高效的代码,很少有编译器会有多个中间产物。优化器相当于一个中间产物到中间产物的转换器,其实就是在中间做了一层加工优化处理,优化器包括移除冗余的计算,去掉执行不到的冗余代码,还有一些其它的可以进行优化的选项。

后端

后端会接收中间产物并转换它到其它语言(如机器码),它也可以链接多个后端去转换代码到一些其它语言。为了产生高效的机器代码,后端应该理解执行代码的体系结构。

LLVM 的功能

LLVM 的核心是负责提供独立于源、目标的优化,并为许多 CPU 架构生成代码。这使得语言开发人员可以只创建一个前端,从源语言生成 LLVM 兼容的 IR 或 LLVM IR。LLVM 不是一个单一项目,它是子项目和其他项目的集合。

    1. LLVM 使用一种简单的低级语言,风格类似C语言
    1. LLVM 是强类型的
    1. LLVM 有严格定义的语义
    1. LLVM 具有精确的垃圾回收
    1. LLVM 提供了各种优化,可以根据需求选择。它具有积极的、标量的、过程间的、简单循环的和概要文件驱动的优化
    1. LLVM 提供了各种编译模型。分别是链接时间、安装时间、运行时和脱机
    1. LLVM 为各种目标架构生成机器码
    1. LLVM 提供 DWARF 调试信息(DWARF 是一种调试文件格式,许多编译器和调试器都使用它来支持源代码级别的调试)

了解了 LLVM 我们就正式进入 WASM 的内容介绍。

WASM 和 JS

它被设计用于高效执行和紧凑表达,它可以以接近原生代码的速度在所有 JS 引擎上执行 (手机、电脑浏览器、Node.js)。每个 WASM 文件都是一个高效、最优且自给自足的模块,称为 WASM 模块,它运行在沙盒上,内存安全,没有权限获取超出沙盒限制以外的东西,WASM 是一个虚拟指令集结构。

JavaScript 是如何执行的?**

    1. 把整个文件加载完成
    1. 将代码解析成抽象语法树
    1. 解释器进行解释然后编译再执行
    1. 最后再进行垃圾回收

JavaScript 既是解释语言又是编译语言,所以 JavaScript 引擎在解析后启动执行。解释器执行代码的速度很快,但它每次解释时都会编译代码。JavaScript 引擎有监视器 (在某些浏览器中称为分析器)。监视器跟踪代码执行情况,如果一个特定的代码块被频繁地执行,那么监视器将其标记为热代码。引擎使用即时 (JIT) 编译器编译代码块。引擎会花费一些时间进行编译,比如以纳秒为单位。花在这里的时间是值得的,因为下次调用函数时,执行速度会比之前快得多,因为编译型代码比解释型代码要快,这个阶段是优化代码阶段。JavaScript 引擎增加了一(或两)层优化,监视器会持续监视代码的执行,监视器标记那些被执行频次更高的代码为高热点代码,引擎将进一步优化这段代码,这个优化需要很长时间。这个阶段产生运行速度非常快的高度优化过的代码,该阶段的优化代码执行速度要比上一段说的优化过的代码还要快得多。显然,引擎在这一阶段花费了更多时间,比如以毫秒为单位,这里耗费的时间将由代码性能和执行效率来进行补偿。JavaScript 是一种动态类型的语言,引擎所能做的所有优化都是基于类型的推断。如果推断失败,那么将重新解释并执行代码,并删除优化过的代码,而不是抛出运行时异常。JavaScript 引擎实现必要的类型检查,并在推断的类型发生变化时提取优化的代码,但是如果重新推断类型,那花在上述代码优化阶段的功夫就白费了。

开发中我们可以通过使用 TypeScript 来防止一些与类型相关的问题,使用 TypeScript,可以避免一些多态代码 (接受不同类型的代码) 的出现。在 JavaScript 引擎中,只接受一种类型的代码总是比多态代码运行得快,但是如果是 TS 里带有泛型的代码,那也会被影响到执行速度。最后一步是垃圾回收,将删除内存中的所有活动对象,JavaScript 引擎中的垃圾回收采用标记清除算法,在垃圾回收过程中,JavaScript 引擎从根对象 (类似于 Node.js 中的全局对象) 开始。它查找从根对象开始引用的所有对象,并将它们标记为可访问对象,它将剩余的对象标记为不可访问的对象,最后清除不可访问的对象。

WASM 是怎么执行的?

WASM 是二进制格式并且已经被编译和优化过了,首先 JS 引擎会去加载 WASM 代码,然后解码并转换成模块的内部表达(即 AST)。这个阶段是解码阶段,解码阶段要远远比 JS 的编译阶段要快。接下来,解码后的 WASM 进入编译阶段,在这个阶段,对模块进行验证,在验证期间,对代码进行某些条件检查,以确保模块是安全的,没有任何有害的代码,在验证过程中对函数、指令序列和堆栈的使用进行类型检查,然后将验证过的代码编译为机器码。由于 WASM 二进制代码已经提前编译和优化过了,所以在其编译阶段会更快,在这个阶段,WASM 代码会被转换为机器码。最后编译过的代码进入执行阶段,执行阶段,模块会被实例化并执行。在实例化的时候,JS 引擎会实例化状态和执行栈,最后再执行模块。WASM 的另一个优点是模块可以从第一个字节开始编译和实例化,因此,JS 引擎不需要等到整个模块被下载,这可以进一步提高 WASM 的性能。WASM 快的原因是因为它的执行步骤要比 JS 的执行步骤少,其二进制代码已经经过了优化和编译,并且可以进行流式编译。但是总的来说,WASM 并不是总是比原生JS 代码执行速度要快的,因为 WASM 代码和 JS 引擎交互和实例化也是要耗费时间的,所以需要考虑好使用场景,在一些简单的计算场景里,WASM 和 JS 引擎的交互时间都会远远超出其本身的执行时间,这种时候还不如直接使用 JS 来编写代码来得快,另一方面,也要减少 WASM 和 JS 引擎之间的数据交互,因为每次两者的数据交互都会耗费一定的时间。

基础部分了解了,下面我们来看一下 WASM 的开发部分。

WASM 开发语言的选择

要写 WASM 应用的话首先不能选用有 GC 的语言,不然垃圾收集器的代码也会占用很大一部分的体积,对 WASM 文件的初始化加载并不友好,比较好的选择就是 C/C++/Rust 这几个没有 GC 的语言,当然使用 Go、C#、TypeScript 这些也是可以的,但是性能也会没有C/C++/Rust 这么好。从上面几个语言来看 Rust 对于前端选手来说会稍微亲切一些,从语法上看和 TS 有一点点的相似(但是学下去还是要比 TS 难得多的), Rust 的官方和社区对于 WASM 都有着一流的支持,而且它也是一门系统级编程语言,有一个和 NPM 一样好用的包管理器 Cargo,同时 Rust 也拥有着很好的性能,用来写 WASM 再好不过了。同时它的社区热度也在不断的上升中。

Rust 开发 WASM

Rust 提供了对 WASM 一流的支持,Rust 无需 GC 、零运行时开销的特点也让它成为了 WASM 的完美候选者。Rust 是怎么编译成 WASM 代码的:


从零开始 WASM 项目

  • Rust 安装

首先需要安装好 Rust的开发环境。安装好之后控制台运行 rustc --version 显示版本号即可。

  • wasm-pack(WASM 打包器)

一个专门用于打包、发布 WASM 的工具,可以用于构建可在 NPM 发布的 WASM 工具包。当我们开发完 WASM 模块时,可以直接使用 wasm-pack publish 命令把我们开发的 WASM 包发布到 NPM 上。使用cargo install wasm-pack 命令来进行安装。

开发环境搭****建

接下来我们举个例子从零开始搭建一个 WASM 开发目录

  • 创建 Rust 工程

首先创建 Rust 工程目录:cargo new example --lib
然后在其目录下控制台运行
npm init -y
package.json 内容如下,配置好 package.json 之后先安装依赖,执行 npm install


{
  "name": "example",
  "version": "0.1.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "build": "rimraf dist pkg && webpack",
    "start": "rimraf dist pkg && webpack-dev-server",
    "test": "cargo test && wasm-pack test --headless"
  },
  "devDependencies": {
    "@wasm-tool/wasm-pack-plugin": "^1.6.0",
    "html-webpack-plugin": "^5.5.0",
    "rimraf": "^3.0.2",
    "webpack": "^5.75.0",
    "webpack-cli": "^4.10.0",
    "webpack-dev-server": "^4.11.1"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

cargo.toml 依赖如下:


[package]
categories = ["wasm"]
description = ""
edition = "2021"
name = "example"
version = "0.1.0"

[lib]
# 一个动态的系统库将会产生,类似于C共享库。当编译一个从其它语言加载调用的动态库时这属性将会被使用
crate-type = ["cdylib"]

[features]

[dependencies]
# 用于将实体从 Rust 绑定到 JavaScript,或反过来。
# 提供了 JS 和 WASM 之间的通道,用来传递对象、字符串、数组这些数据类型
wasm-bindgen = "0.2.83"
wee_alloc = {version = "0.4.5", optional = true}

# web-sys 可以和 JS 的 API 进行交互,比如 DOM
[dependencies.web-sys]
features = ["console"]
version = "0.3.60"

[dev-dependencies]
# 用于所有JS环境 (如Node.js和浏览器)中的 JS 全局对象和函数的绑定
js-sys = "0.3.60"

# 0 – 不优化
# 1 – 基础优化
# 2 – 更多优化
# 3 – 全量优化,关注性能时建议开启此项
# s – 优化二进制大小
# z – 优化二进制大小同时关闭循环向量,关注体积时建议开启此项
[profile.dev]
debug = true
# link time optimize LLVM 的链接时间优化,false 时只会优化当前包,true/fat会跨依赖寻找关系图里的所有包进行优化
# 其它选项还有 off-关闭优化,thin是fat的更快版本
lto = true
opt-level = 'z'

[profile.release]
debug = false
lto = true
opt-level = 'z'
  • 内存分配器(可选)

上面我们在依赖中加入了 wee_alloc 这个内存分配器,对比默认的 10kb 大小的分配器,它只有 1kb 的大小,但是它要比默认的分配器速度要慢,所以默认不开启,为减少模块打包时的大小,可以使用这个内存分配器。在src/lib.rs 中使用的代码如下:


#[cfg(feature = "wee_alloc")]
#[global_allocator]
static ALLOC: wee_alloc::WeeAlloc = wee_alloc::WeeAlloc::INIT;
  • 入口文件

项目根目录下创建 index.html 文件,写入以下内容:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>WASM Hello World</title>
  </head>
  <body></body>
  <script src="index.js"></script>
</html>
  • Webpack 配置

项目根目录下新建 webpack.config.js 并新建 js/index.js 文件用于调用 WASM 侧暴露的函数。WasmPackPlugin 这个插件会帮我们在运行 Webpack 时自动去打包 WASM ,生成可直接用于发布的 NPM 模块。


const path = require("path");
const HtmlWebpackPlugin = require("html-webpack-plugin");
const WasmPackPlugin = require("@wasm-tool/wasm-pack-plugin");

const dist = path.resolve(__dirname, "dist");

module.exports = {
  mode: "development",
  entry: {
    index: "./js/index.js",
  },
  output: {
    path: dist,
    filename: "[name].js",
  },
  plugins: [
    new HtmlWebpackPlugin({
      template: path.resolve(__dirname, "./index.html"),
      inject: false,
    }),
    new WasmPackPlugin({
      crateDirectory: __dirname,
    }),
  ],
  experiments: {
    asyncWebAssembly: true,
  },
  resolve: {
    extensions: [".ts", ".js"],
  },
};

上面的所有配置都完成之后,命令行执行 npm start 即可启动项目,然后会自动生成 pkg 目录,这个就是最终可以发布打包到 NPM 上的的 WASM 库。最终的项目目录长这个样子:

Hello World

在上面的环境搭建好之后,我们就开始试着在浏览器控制台上打印出 Hello World 吧,进入到 src/lib.rs 文件,写入以下代码:

QR Code
微信扫一扫,欢迎咨询~

联系我们
武汉格发信息技术有限公司
湖北省武汉市经开区科技园西路6号103孵化器
电话:155-2731-8020 座机:027-59821821
邮件:tanzw@gofarlic.com
Copyright © 2023 Gofarsoft Co.,Ltd. 保留所有权利
遇到许可问题?该如何解决!?
评估许可证实际采购量? 
不清楚软件许可证使用数据? 
收到软件厂商律师函!?  
想要少购买点许可证,节省费用? 
收到软件厂商侵权通告!?  
有正版license,但许可证不够用,需要新购? 
联系方式 155-2731-8020
预留信息,一起解决您的问题
* 姓名:
* 手机:

* 公司名称:

姓名不为空

手机不正确

公司不为空