如果說以一個前端工程師來說,學習 Rust 除了練到一些系統程式之外,另一個好處就是 Rust 可以編譯成 WebAssembly(wasm)。
跟 JavaScript 比起來,wasm 是相對更低階、效能更好的可執行程式,目前主流的瀏覽器都有支援,甚至有不少厲害的產品都已經用它開發,像是設計師們很喜歡用的 Figma,還有近期 Adobe 的 PhotoShop 線上版,都是 wasm 做出來的。
雖然 wasm 本身其實不太好寫,目前已有不少程式語言可以直接進行轉換,例如 C、C++、Go 等程式語言都行,Rust 也是其中一個,也就是說,我們可以其它程式語言寫,最後編譯成 wasm 檔案。
wasm 目前通常是用在比較效能吃緊的地方,雖然我們現在還沒有什麼需要拼效能的地方,但還是就來做一個計算 Fibonacci 的功能試試看吧
要把專案編譯成 wasm 有些前置動作,根據官網手冊的說明,會先需要安裝 wasm-pack
程式,我們就直接用 cargo
指令來安裝它:
$ cargo install wasm-pack
要注意的是這裡不是 add
而是 install
,add
是把套件加進 Cargo.toml
裡,然後在這個專案裡就能使用它;而 install
是把程式安裝到系統裡,在任何地方都能獨立執行。
這個我們之前有學過,用 --lib
就能搞定:
$ cargo new fib --lib
Created library `fib` package
接著打開 src/lib.rs
寫上可以計算 Fibonacci 的函數:
pub fn fib(n: i32) -> u64 {
if n <= 0 {
panic!("不能小於或等於零");
}
match n {
1 => 1,
2 => 1,
3 => 2,
_ => fib(n - 1) + fib(n - 2),
}
}
不算太難,用 Recursive 搭配 match
做 Pattern Matching 寫起來還滿簡單的。
接著,在這個專案裡還需要裝個套件:
$ cargo add wasm-bindgen
這是用來讓 Rust 與 JavaScript 兩邊可以互相溝通的東西。最後,在 Cargo.toml
裡的 [lib]
段落裡還要再加一段 crate-type = ["cdylib"]
的設定,如果 [lib]
不存在就自己手動加上去,最後檔案的內容看起來會像這樣:
[package]
name = "fib"
version = "0.1.0"
edition = "2021"
# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html
[lib]
crate-type = ["cdylib"]
[dependencies]
wasm-bindgen = "0.2.87"
wasm-bindgen
的版號可能會隨著時間而有所不同。
再回到剛剛寫好的函數前面加上專用的屬性:
use wasm_bindgen::prelude::*;
#[wasm_bindgen]
pub fn fib(n: i32) -> u64 {
// ... 略 ...
}
這樣就可以準備來進行編譯了。
要把 Rust 專案編譯成 wasm 的話可以使用剛才安裝的 wasm-pack
程式並指定編譯的 target:
$ wasm-pack build --target web
[INFO]: 🎯 Checking for the Wasm target...
[INFO]: 🌀 Compiling to Wasm...
Compiling proc-macro2 v1.0.69
Compiling unicode-ident v1.0.12
Compiling wasm-bindgen-shared v0.2.87
... 略 ...
Compiling fib v0.1.0 (/private/tmp/fib)
Finished release [optimized] target(s) in 3.73s
[INFO]: ⬇️ Installing wasm-bindgen...
[INFO]: Optimizing wasm binaries with `wasm-opt`...
[INFO]: Optional fields missing from Cargo.toml: 'description', 'repository', and 'license'. These are not necessary, but recommended
[INFO]: ✨ Done in 3.92s
[INFO]: 📦 Your wasm pkg is ready to publish at /private/tmp/fib/pkg.
這樣就編譯完成了!
編譯完成的檔案會在 pkg
目錄裡,所以我另外建一個全新的 HTML 專案,裡面擺一個 index.html
跟 app.js
,不想分開寫也可以省略 app.js
,直接在 HTML 裡寫 <script>
也行。最後把剛剛編譯好的 pkg
目錄搬一份過來,現在目錄大概會像這樣:
├── app.js
├── index.html
└── pkg
├── fib.d.ts
├── fib.js
├── fib_bg.wasm
├── fib_bg.wasm.d.ts
└── package.json
接著編輯 index.html
:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Document</title>
</head>
<body>
<script type="module" src="app.js"></script>
</body>
</html>
app.js
的內容如下:
import init, { fib } from "./pkg/fib.js";
init().then(() => {
console.log(fib(20));
});
這裡從剛剛編譯好的檔案裡的其中一個 .js
檔 import 東西進來,fib
就是剛剛在 Rust 裡寫的那個函數。最後一步,打開瀏覽器檢視 index.html
,應該就會看到 Console 印出東西來了:
這裡會看到尾巴有個 n
是因為這是一個 JavaScript 的 BigInt
的型態,如果想要轉成數字再加個 Number()
就行了。
就這樣,我們在 Rust 裡寫好的程式,就能編譯成 wasm,然後給 JavaScript 呼叫了 :)