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
語法導入,這個狀態下的 var
與 function
不再是 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 使用 &str
或 String
類型,但是 return value 使用 String
類型,以避免生命週期問題。
由於手續繁瑣,不建議直接將 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
: 可不斷呼叫,僅讀取捕捉的變數。可相容FnMut
和FnOnce
。- 這三個 trait 有著特殊語法
Fn(Arg...) -> Ret
,代表其 signature,跟函式指標fn(Arg...) -> Ret
的語法一樣。
只要 callable 捕捉外部變數,生命週期就不是 'static
,因為 callable 本身的生命週期必須小於所有被捕捉的變數。可以利用 Arc<Atomic??>
或 Arc<RwLock<T>>
包裝並移入 (move) 複製後的變數,這樣就可以產生靜態生命週期,類似跨執行緒的概念。其中 Fn + 'static
與 FnMut + '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
套件來處理。