background

Trying Webassembly in Rust

2020/12/13

本篇由 2019/1/17 撰寫之 https://5xruby.tw/posts/webassembly-run-native-code-on-browser-using-rust/ 修改而來

跑在瀏覽器上的組合語言?為了提供比 Javascript 更快速的載入/執行速度,或是純粹不想寫 Javascript...Webassembly 這個標準被提出了(以下簡稱 wasm),可以是 C/C++ 這種靜態語言的編譯目標,也就是我們可以把現有的 C/C++ 專案編譯成 wasm,有人就嘗試把 ffmpeg 編譯到瀏覽器上執行,後來 golang 以及 rust 也可以編譯成 wasm ,今天來用 rust 試試看 wasm 吧

基本上這篇我是看著 https://rustwasm.github.io/docs/book 做的

安裝 Rust

官方建議透過 rustup 來安裝 rusthttps://www.rust-lang.org/tools/install

我對 rustup 的理解: rust 官方的 rust 版本管理器

安裝完成之後,安裝現在的 rust 穩定版

rustup default stable

準備 rust wasm 開發環境以及工具

為了要執行 wasm 程式,必須先用 js 載入 wasm,接著用 js 呼叫 wasm 的 exports;但是要怎麼讓編譯器知道哪些東西要給 js 呼叫?js 跟 rust 互傳變數/資料的時候如何各自表述?甚至載入 wasm 可能各家瀏覽器的 API 都不盡相同...幸好 rust wasm 已經有許多現成的工具包幫我們解決這些問題,所以接下來要先來把開發環境準備好

wasm-pack & cargo-generate

  • wasm-pack 是 rust 跟 js 橋樑的核心,它的 macro 讓指定的 rust 函式以及資料結構提供給 js 端使用,當然也就處理了兩邊的資料轉換,最後它還幫忙生成 js 讓 webapp 端可以直接 import 使用
  • cargo-generate 讓我們可以透過 cargo generate 依照指定模板建立新 rust 專案
cargo install wasm-pack cargo-generate

cargo 是 rust 的 npm / gem / bundle,我的理解是這樣

建立專案

這邊建立一個叫做 wasm-glifewasm-pack-template 專案:

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

建立專案後,立刻來編譯 rust 到 wasm 試試:

rustup run stable wasm-pack build

編譯的結果會產生在 ./pkg 下,可以看到這樣的專案架構:

├── Cargo.toml <= 給 rust 的 package.json
├── Cargo.lock <= 給 rust 的 package-lock.json / yarn.lock
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
├── target/ <= rust 以 wasm32 作為 target 編譯出來的結果
├── pkg <= rust 編譯後對 target/ 包裝成可直接使用的 ES module / node_module
│   ├── package.json
│   ├── README.md
│   ├── wasm_game_of_life_bg.js
│   ├── wasm_game_of_life_bg.wasm
│   ├── wasm_game_of_life_bg.wasm.d.ts
│   ├── wasm_game_of_life.d.ts
│   └── wasm_game_of_life.js
├── src/ <= rust 原始碼
│   ├── lib.rs
│   └── utils.rs
└── tests/ <= rust 測試
    └── web.rs

Webapp 部份

要給瀏覽器跑,我們需要 HTML,也就是需要一個 webapp,這邊 rustwasm 直接提供了一個模板來幫助我們快速建立 webapp:

npm init wasm-app www

接著修改 www/package.json,接下來 wasm 就可以寫 import "wasm-glife" 進來用:

"url": "https://github.com/rustwasm/create-wasm-app/issues"
},
"homepage": "https://github.com/rustwasm/create-wasm-app#readme",
+ "dependencies": {
+ "wasm-glife": "file:../pkg"
+ },
"devDependencies": {
- "hello-wasm-pack": "^0.1.0",
"webpack": "^4.29.3",
"webpack-cli": "^3.1.0",
"webpack-dev-server": "^3.1.5",

Rust 專案產生的內容

Cargo.toml
[dependencies]
wasm-bindgen = "0.2.63"

這邊最重要的就是 wasm-bindgen 這個套件,待會會看到相關的 macro

src/lib.rs

wasm-pack-template 產生的 src/lib.rs 一開始引入一些東西,configure 一些東西,先不管他,下面這兩個已經完整示範了要怎麼在 rust 使用 js function 以及把 rust function 提供給 js 使用

#[wasm_bindgen]
extern {
fn alert(s: &str);
}
#[wasm_bindgen]
pub fn greet() {
alert("Hello, wasm-glife!");
}
www/index.js
import * as wasm from "hello-wasm-pack";
wasm.greet();

js 這邊就把 wasm 當成一般的 ES module 引入,接著呼叫 greet()

但是我們現在還沒編譯 rust 到 wasm,這時直接去跑 webpack 是編譯不過的,畢竟 hello-wasm-pack 還沒編譯出來

讓 webapp 呼叫 rust greet()

首先修改 www/index.js 改用我們建立的 wasm-glife

-import * as wasm from "hello-wasm-pack";
+import * as wasm from "wasm-glife";

接著去把開發模式跑起來:

cd www
npm install
npm start

支援 wasm 的瀏覽器打開 http://localhost:8080/ 應該可以看到:

hello wasm

在打開瀏覽器開發工具切到 Network tab,確實可以看到 wasm 被載入了:

network debugger

wasm 是 binary 格式的,需要用工具才能轉回可讀的文字:

wasm preview

最後偷瞄一下 webpack 怎麼幫我們載入 wasm 的 (bootstrap.js):

webpack load wasm

接下來就看要做什麼了

如果改了 rust 那邊的程式,再下一次這個指令就可以重新編譯 wasm-glife:

rustup run stable wasm-pack build

webpack-dev-server 跑著的狀況下,也會讓瀏覽器自動 reload 讀取新版程式碼

Conway's game of life

筆者看的這份教學就是要來做 Conway's Game of Life;基本上就是在一個棋盤上,每個格子可以是死亡或是存活兩種狀態,並且會根據周圍 8 格鄰居格子的狀態決定下回合的狀態

但是我對 rust 還沒這麼熟,所以就邊看邊抄,邊抄邊學,嘗試寫成自己的版本:

https://static.pastleo.me/rs-wasm-glife-20201213/

Repository: https://github.com/pastleo/rs-wasm-glife/

蠻好玩的,我可以玩很久

一些實做細節:

心得

其實 conway's game of life 用 rust -> wasm 很可能沒有比純 js 實做來的快,說不定光是在 rust 跟 js 之間的資料轉換的效能犧牲就已經不值得了,現在 wasm 是以 MVP 的概念先在各家瀏覽器支援,我個人比較期待的是這幾個功能:

  • SIMD 使 wasm 支援類似 GPU 的單指令多資料,目前看起來蠻有希望的(stage 3),這個很可能可以有真正地效能提昇
  • threading 讓支援 threading 加速的 C/C++ 程式發揮優勢
    • 其實如果有 SharedArrayBuffer,加上目前的 Javascript worker 其實就可以做到了
  • GC 讓瀏覽器執行 ruby, erlang, elixir 等 runtime