Webassembly - run native code on browser using Rust

跑在瀏覽器上的組合語言?在 ban 了 flash 之後,大家在尋找是否有其他可以在瀏覽器上執行原生程式的方案,又或者只是不想寫 Javascript…Webassembly 這個標準被提出了(以下簡稱 wasm),可以是 C/C++ 這種靜態語言的編譯目標,也就是我們可以把現有的 C/C++ 專案編譯成 wasm,有人就嘗試把 ffmpeg 編譯到瀏覽器上執行,後來 golang 以及 rust 也可以編譯成 wasm ,今天來用 rust 試試看 wasm 吧

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

Rust

我選擇 rust 純粹只是因為我沒用過,聽說對於 concurrent 有一套很獨特的概念,想嘗試看看而已,首先來把 rust (rustup) 安裝起來:https://www.rust-lang.org/tools/install

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

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

rustup toolchain install stable
rustup default stable

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

基本上現在使用 wasm 的方法是比較像是寫底層原生程式給 js 呼叫,必須先用 js 載入 wasm,接著用 js 呼叫 wasm 的 exports

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

wasm-pack

首先 wasm-pack 是 rust 跟 js 橋樑的核心,它的 macro 讓指定的 rust 函式以及資料結構提供給 js 端使用,當然也就處理了兩邊的資料轉換,最後它還幫忙生成 js 讓 webapp 端可以直接 import 使用

cargo install wasm-pack

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

cargo-generate & wasm-pack-template

同時也有人弄了新手模板 wasm-pack-template,但是為了使用他我們要裝 cargo-generate,於是乎:

cargo install cargo-generate

create-wasm-app

rust 的部份準備好了,但是我們要怎麼弄個能夠載入 wasm 的 webapp?還有個 wasm webapp 的新手模板 create-wasm-app,這個幫你把 webpack 設定值都準備好,而且就是拿來跟 wasm-pack-template 搭配使用的,但是也意味著我們需要 nodejs,而且我用 nodejs 11.6.0 才能用 npm 從模板建立專案

node -v # => v11.6.0
npm install -g create-wasm-app

終於可以來建立專案囉

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

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

還沒完,還要把 webapp 建立起來,弄個 www 資料夾來放 webapp:

mkdir www
cd www
npm init wasm-app
npm install

看來產生不少東西,看一下哪些檔案值得我們關心

tree .
.
├── Cargo.toml <= 給 rust 的 package.json
├── LICENSE_APACHE
├── LICENSE_MIT
├── README.md
├── src
│   ├── lib.rs <= 將會是接下來的重頭戲,我們會在這邊定義給 js 呼叫的東西
│   └── utils.rs
├── tests
│   └── web.rs
└── www <= 大家熟悉的 webpack 專案
    ├── bootstrap.js
    ├── index.html
    ├── index.js <= 將會在這邊呼叫 rust wasm
    ├── LICENSE-APACHE
    ├── LICENSE-MIT
    ├── package.json
    ├── package-lock.json
    ├── README.md
    └── webpack.config.js

Cargo.toml

[dependencies]
cfg-if = "0.1.2"
wasm-bindgen = "0.2"

這邊最重要的就是 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 還沒編譯出來

編譯 rust 到 wasm

rustup run stable wasm-pack build -d www/wasm
  • rustup run stable ... 的部份,我想是類似 bundle exec ...,去執行對應 rust 版本的執行檔
  • wasm-pack build 就能編譯這個 rust 專案成一個 node module
    • -d www/wasm 則是讓編譯出來的 node module 放在 www/wasm

按下 enter 之後就會開始下載 dependency,編譯,第一次編譯約莫幾分鐘

線上書的做法是產生一個真的 local npm module…但是我覺得沒必要這麼麻煩,直接把東西丟在 webpack 專案裡頭就能用了

串到 webapp

wasm-pack 成功之後會產生蠻多東西的,不過我們 focus 在產生的 node module

tree .
...
└── www
    ├── bootstrap.js
    ├── index.html
    ├── index.js
    ├── LICENSE-APACHE
    ├── LICENSE-MIT
    ├── package.json
    ├── package-lock.json
    ├── README.md
    ├── wasm <= wasm-pack 編譯出來的 node module
    │   ├── package.json <= "module": "wasm_glife.js"
    │   ├── README.md
    │   ├── wasm_glife_bg.d.ts
    │   ├── wasm_glife_bg.wasm <= 就是我,wasm
    │   ├── wasm_glife.d.ts
    │   └── wasm_glife.js <= wasm-pack 會把必要的橋接產生在這
    └── webpack.config.js

因為產生的是 node module,透過 webpack 可以直接 import 使用,我們只要把 www/index.js import 改成

import * as wasm from "./wasm";

接著把 webpack 跑起來

npm run start

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

hello wasm

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

network debugger

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

wasm preview

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

webpack load wasm

實做一些東西來玩玩:Conway’s game of life

因為我看的這本線上書就是要來做 Conway’s Game of Life,基本上就是在一個棋盤上,每個格子可以是死亡或是存活兩種狀態,並且會根據周圍 8 格鄰居格子的狀態決定下回合的狀態

但是我對 rust 還沒這麼熟,所以就邊看邊抄,邊抄邊學,嘗試寫成自己的版本:https://github.com/pastleo/rs-wasm-glife/

我也把東西放到網站上大家可以上去玩玩:https://static.pastleo.me/rs-wasm-glife/

蠻好玩的,我可以玩很久

心得

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

  • threading 讓支援 threading 加速的 C/C++ 程式發揮優勢
  • GC 讓瀏覽器執行 ruby, erlang, elixir 等 runtime

這幾天就花點時間試試看 wasm,體驗一下在自己機器開發/執行的感覺(話說在 android chrome 跟 ios safari 都跑得起來)順便學一下 rust