Skip to content

Instantly share code, notes, and snippets.

@KmolYuan
Last active January 14, 2022 06:18
Show Gist options
  • Save KmolYuan/73a9799433227e3d1cd7124cb9d692da to your computer and use it in GitHub Desktop.
Save KmolYuan/73a9799433227e3d1cd7124cb9d692da to your computer and use it in GitHub Desktop.

Rust WASM 指南

JavaScript 到 Rust

JavaScript (JS) 可以將物件傳入 Rust,使用 wasm_bindgen::JsValue 類型表示。實際上就是 JS 的 reference counter 系統,與 Rust 的 Arc<RwLock<T>> 類似,但是可以自動鎖定進行寫入讀取。

但是若要細分 JS 物件,要引入 js-sys crate 提供更細節的類型轉換,有點在 C 語言裡撰寫 Python 程式的味道,因此不建議在這邊蹚渾水。Function 就是其中一例,建議直接寫好 JS 的函式,不要跟 Rust 寫在一起。註冊到瀏覽器的 window 物件後,等於是變成了 global 物件,Rust 就可以找到了。

要注意的是,若 wasm-pack 選擇 "web" 方式編譯,會變成 ECMAScript 的 module 模式,也建議用 import 語法導入,這個狀態下的 varfunction 不再是 global 變數,必須寫入 window 物件。

// main.js
import * as wasm from "./pkg/my_wasm.js"; // 導入,必須寫在最上方

window.my_function = () => {/* 定義全域函式 */};

wasm.default().then(() => {/* 初始化後的行為 */});

其中 JS 的函式表達式分為一般函式與箭頭函式 (Arrow Function),差異在於箭頭函式不具有 scope 邊界。因此 var 變數的生命週期this 關鍵字指向的 scope 會是上一層,最上層為 global。

而 Rust 端直接聲明外部函式即可,也可以使用 JS 內建的 global 函式。

#[cfg(target_arch = "wasm32")]
#[wasm_bindgen]
extern "C" {
    fn alert(s: &str); // 內建的 window.alert 函式
    fn my_function();
}

函式的參數最多 7 個,原始類型 (permitive type) 可以自動轉成 JS 物件,或是標示成 JsValue 保持混合類型再用 js-sys 操作。要注意的是 input parameter 使用 &strString 類型,但是 return value 使用 String 類型,以避免生命週期問題。

Rust 到 JavaScript

由於手續繁瑣,不建議直接將 Rust 類型包裝給 JS 使用,僅在需要效能的部份給 Rust 處理即可。然而,JS 的功能大多都是異步 (asynchronous) 的,因此無法直接得到 return value。通常使用 callback 函式即可解決,例如:

done => do_something().then(e => done(e.result));

上面的範例傳入 done 函式作為參數,並且在 do_something 函式產生任務後,使用 Promise.then 方法計算並接收 return value。(詳見異步處理的程式設計方式)

Rust 這邊會建立 JS 的 callback 物件,不過必須考量其生命週期。所有 callable 可分為下面三種 trait:

  • FnOnce: 呼叫一次後摧毀。
  • FnMut: 可不斷呼叫,並且修改捕捉的變數。可相容 FnOnce
  • Fn: 可不斷呼叫,僅讀取捕捉的變數。可相容 FnMutFnOnce
  • 這三個 trait 有著特殊語法 Fn(Arg...) -> Ret,代表其 signature,跟函式指標 fn(Arg...) -> Ret 的語法一樣。

只要 callable 捕捉外部變數,生命週期就不是 'static,因為 callable 本身的生命週期必須小於所有被捕捉的變數。可以利用 Arc<Atomic??>Arc<RwLock<T>> 包裝並移入 (move) 複製後的變數,這樣就可以產生靜態生命週期,類似跨執行緒的概念。其中 Fn + 'staticFnMut + 'static 的約束條件是相等的。

let atomic = Arc::new(AtomicBool::new(false));
let rw_lock = Arc::new(RwLock::new(...));
let a = atomic.clone();
let r = rw_lock.clone();
let callable = move || {
    let a = a.load(Ordering::Relaxed);
    let r = r.read().unwrap();
    ...
};

有了靜態週期的 callable 後,透過 wasm_bindgen::closure::Closure 類型,

  • FnOnce + 'static 可以用 Closure::once_into_js 包裝成 JsValue
  • Fn + 'static 可以用下面的方法建立 JsValue
let callable = Closure::<dyn Fn()>::wrap(Box::new(callable)).into_js_value();

其中 Box 是 Rust 的智慧指標,dyn trait 語法是用 fat pointer 提供 trait 層的指標,因為 closure 本身是不同類型的,除了泛型還有泛化指標可用。包裝成 JsValue 時,便是將指標內容從 WASM 移轉到 JS 的 reference counter 中。

補充:異步執行

支援異步執行的程式語言可能會有所謂「異步函式」與「異步物件」兩種概念。異步函式通常只是個語法糖,實際上是回傳異步物件的函式。JS 中的異步物件稱為 "Promise";C++、Rust 和 Python 的稱為 "Future",不過 Rust 沒有限定類型來實作,Future 僅為一種 trait。

let async_func1 = () => new Promise((done, error) => {/* 定義 */});
let async_func2 = async () => {/* 定義 */};

async_func1().then(() => {/* done */}, () => {/* error */});
async_func2().then(() => {/* done */}, () => {/* error */});

而異步物件不會執行,它們通常需要一個執行器 (executor) 演算法來處理,包含排程順序、錯誤處理、是否使用多執行緒等等。像 Rust 依賴第三方套件提供執行器,C++ 則是內建一個(但是仍然是函式導向),而 JS 與 Python 等依賴自身的直譯器執行。WASM 裡的 Rust 不可以有執行器,必須用 wasm-bindgen-futures 套件來處理。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment