在上一章節中,我們有提到WebAssembly比JavaScript的效能還要快很多,但始終沒有舉出一個例子來比較它們實際的效能差異。在這篇文章中,會介紹如何把任意現有的Rust函式庫編譯成WebAssembly,然後移植到網頁瀏覽器上執行,並且比較Rust函式庫做出來的WebAssembly模組以及原生JS模組在網頁瀏覽器上執行的效能差異。
ShortCrypt
ShortCrypt是筆者開發的跨程式語言的資料加解密函式庫,其介紹文章的網址如下:
在本篇文章中,將會嘗試把用Rust程式語言實作的ShortCrypt函式庫編譯成WebAssembly模組,並與用原生JS程式實作的ShortCrypt一同用Webpack打包,直接在網頁瀏覽器上實際測試Rust-WebAssembly實作和JS實作的效能差異。
ShortCrypt WebAssembly
建立專案目錄
要將現有的Rust函式庫編譯成WebAssembly模組,不一定需要去修改原本Rust函式庫程式專案的程式碼。我們同樣可以依照上一章介紹的方式,利用wasm-pack-template
模板建立出一個新的Cargo程式專案,名為short-crypt-webassembly
。
指令如下:
引用short-crypt
套件(crate)
編輯Cargo.toml
,在[dependencies]
區塊下加上:
引用serde
框架
為了方便實作,我們會引用serde
框架來協助處理序列化和反序列化的動作。另外,為了讓產生出來的JS的膠水代碼也能夠支援serde
框架的序列化和反序列化,我們還要使用serde-wasm-bindgen
套件。
編輯Cargo.toml
,在[dependencies]
區塊下加上:
serde-wasm-bindgen = "*"
Rust程式實作
由於#[wasm_bindgen]
屬性無法用在非目前這個crate底下的結構體、列舉、或是函數、方法,因此我們要另外建立一個Wrapper結構體,將short-crypt
這個crate下的ShortCrypt
結構體包裹起來。
程式如下:
...
#[wasm_bindgen]
pub struct ShortCrypt(short_crypt::ShortCrypt);
接著參考ShortCrypt的API文件,將想要暴露(export)給JavaScript使用的函數、方法寫進有套用#[wasm_bindgen]
屬性的impl
區塊中。程式如下:
...
use serde::{Deserialize, Serialize};
...
#[derive(Serialize, Deserialize)]
struct Cipher {
pub base: u8,
pub body: Vec<u8>,
}
#[wasm_bindgen]
impl ShortCrypt {
#[wasm_bindgen(constructor)]
pub fn new(key: &str) -> ShortCrypt {
ShortCrypt(short_crypt::ShortCrypt::new(key))
}
pub fn encrypt_string(&self, plaintext: &str) -> JsValue {
let (base, body) = self.0.encrypt(plaintext);
serde_wasm_bindgen::to_value(&Cipher {
base,
body,
})
.unwrap()
}
pub fn encrypt_binary(&self, plaintext: &[u8]) -> JsValue {
let (base, body) = self.0.encrypt(plaintext);
serde_wasm_bindgen::to_value(&Cipher {
base,
body,
})
.unwrap()
}
pub fn decrypt_to_binary(&self, cipher: JsValue) -> Option<Vec<u8>> {
let cipher: Cipher = serde_wasm_bindgen::from_value(cipher).ok()?;
self.0.decrypt(&(cipher.base, cipher.body)).ok()
}
pub fn decrypt_to_string(&self, cipher: JsValue) -> Option<String> {
let cipher: Cipher = serde_wasm_bindgen::from_value(cipher).ok()?;
match self.0.decrypt(&(cipher.base, cipher.body)) {
Ok(v) => String::from_utf8(v).ok(),
Err(_) => None,
}
}
pub fn encrypt_string_to_url_component(&self, data: &str) -> String {
self.0.encrypt_to_url_component(data)
}
pub fn encrypt_binary_to_url_component(&self, data: &[u8]) -> String {
self.0.encrypt_to_url_component(data)
}
pub fn decrypt_url_component_to_binary(&self, url_component: &str) -> Option<Vec<u8>> {
self.0.decrypt_url_component(url_component).ok()
}
pub fn decrypt_url_component_to_string(&self, url_component: &str) -> Option<String> {
match self.0.decrypt_url_component(url_component) {
Ok(v) => String::from_utf8(v).ok(),
Err(_) => None,
}
}
pub fn encrypt_string_to_qr_code_alphanumeric(&self, data: &str) -> String {
self.0.encrypt_to_qr_code_alphanumeric(data)
}
pub fn encrypt_binary_to_qr_code_alphanumeric(&self, data: &[u8]) -> String {
self.0.encrypt_to_qr_code_alphanumeric(data)
}
pub fn decrypt_qr_code_alphanumeric_to_binary(&self, url_component: &str) -> Option<Vec<u8>> {
self.0.decrypt_qr_code_alphanumeric(url_component).ok()
}
pub fn decrypt_qr_code_alphanumeric_to_string(&self, url_component: &str) -> Option<String> {
match self.0.decrypt_qr_code_alphanumeric(url_component) {
Ok(v) => String::from_utf8(v).ok(),
Err(_) => None,
}
}
}
以上程式中,比較需要特別注意的部份有以下幾點:
- Rust程式語言提供的
i8
、u32
、f64
等基本資料型別(不包括128位元的整數)可以直接與JavaScript的Number型別的值做對應。而String
字串結構體和&str
字串切片則可以直接與JavaScript的字串型別的值做對應。Vec<T>
可以直接與JavaScript的陣列型別的值做對應。 wasm_bindgen
提供的JsValue
可以與JavaScript的任意值做對應。#[wasm_bindgen(constructor)]
屬性可以用來指定要把哪個關聯函數當作是JavaScript物件的建構子(constructor)。也就是在JS中使用new關鍵字時會呼叫的函數。- 因為
wasm_bindgen
不支援有使用泛型的函數,所以這邊把原先有用到泛型的函數拆開來寫了(將輸入和輸出的值分為binary
和string
版本)。 Option
列舉的變體None
,對應著JavaScript的undefined
。
然後您可能還會有一個疑問:為什麼這邊要使用JsValue
結構體,怎麼不要像以下這樣也把Cipher
結構體加上#[wasm_bindgen]
屬性,使它能夠直接被函數回傳,或是直接作為函數的參數來傳入?
#[wasm_bindgen]
pub struct Cipher {
pub base: u8,
pub body: Vec<u8>,
}
是的,這樣的確是非常直覺的作法,但這實際上會讓程式編譯失敗,因為Vec<T>
並沒有實作Copy
特性,它無法作為JavaScript物件的公開(public)欄位。
建置專案
寫完Rust程式後,就可以執行以下指令將它編譯成.wasm
檔案,以及產生出相對應的膠水代碼!
把WebAssembly套用在HTML網頁
依照上一章介紹的方式,我們可以在Cargo程式專案目錄中,新增一個www
目錄,並完成基本的TypeScript+Webpack程式專案。然後再繼續進行下面的修改。
在www
目錄執行以下指令加入short-crypt
套件:
import { ShortCrypt } from "short-crypt";
import { ShortCrypt as ShortCryptWASM } from "../../pkg/short_crypt_webassembly.js";
const TIMES = 100;
const KEY = "magickey";
const TEXT = "articles";
class BenchResult {
constructor(public readonly result1: string, public readonly result2: string, public readonly duration: number) {
}
printLog() {
console.log((this.duration / TIMES * 1000) + "us", this.result1, this.result2);
}
}
export const benchShortCrypt = () => {
let result1 = "";
let result2 = "";
let duration = 0;
for (let i = 0;i < TIMES;i++) {
const start = performance.now();
const sc = new ShortCrypt(KEY);
result1 = sc.encryptToURLComponent(TEXT);
result2 = sc.encryptToQRCodeAlphanumeric(TEXT);
duration += performance.now() - start;
}
return new BenchResult(result1, result2, duration);
};
export const benchShortCryptWASM = () => {
let result1 = "";
let result2 = "";
let duration = 0;
for (let i = 0;i < TIMES;i++) {
const start = performance.now();
const sc = new ShortCryptWASM(KEY);
result1 = sc.encrypt_string_to_url_component(TEXT);
result2 = sc.encrypt_string_to_qr_code_alphanumeric(TEXT);
duration += performance.now() - start;
}
return new BenchResult(result1, result2, duration);
};
import { benchShortCrypt, benchShortCryptWASM } from "./lib.js";
benchShortCrypt().printLog();
benchShortCryptWASM().printLog();
在Webpack的進入點中,我們分別測量了原生JavaScript版本的ShortCrypt和WebAssembly版本的ShortCrypt的執行時間。會將結果輸出到網頁瀏覽器的主控台(console)中。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8"/>
<title>ShortCrypt</title>
</head>
<body>
Please see the console.
<script src="./js/bundle.min.js"></script>
</body>
</html>
然後就可以在www
目錄中執行npm run build:webpack
指令來打包專案。接著再使用VSCode的Live Server開啟Webpack輸出的index.html
,正常情況下會成功在網頁瀏覽器的主控台輸出結果。
在Firefox 60.7.2esr上的執行結果如下:
40us 2E87Wx52-Tvo 3BHNNR45XZH8PU
在Chromium 75.0.3770.90上的執行結果如下:
24.650000013934914us 2E87Wx52-Tvo 3BHNNR45XZH8PU
由此可知WebAssembly版本的ShortCrypt的效能大概是原生JavaScript版本的20倍!
適合或不適合編譯成WebAssembly的Rust程式
WebAssembly固然強大,但我們並沒有辦法將所有的Rust程式都移植成WebAssembly程式。像是若Rust程式有用到C/C++函式庫,那它就無法被成功編譯為WebAssembly程式。
在crates.io上有很多Rust的套件可以取用,其中很適合用於WebAssembly的分類有:不使用標準函式庫的no-std
、解析資料的parser-implementations
和text-processing
、讓Rust語法更方便的rust-patterns
。而有些套件則是原本就專門針對WebAssembly來設計,會被分在wasm
。
總結
在這個章節中,我們學習了把現有Rust函式庫編譯為WebAssembly程式的方式,並且對於WebAssembly的程式的效能有所認識。從下一章節開始,要開始實作大一點的專案,利用WebAssembly來高效地操作HTML的Canvas,並使它能與使用者互動。
下一章:實作康威生命遊戲。