在 TypeScript 中使用 WebAssembly
本文將說明如何在 TypeScript 中使用 WebAssembly。
我們將介紹整合 TypeScript 和 WebAssembly 的實用且易懂的方法。
YouTube Video
在 TypeScript 中使用 WebAssembly
WebAssembly(Wasm)是一種可以在瀏覽器內以接近原生速度運行的二進位格式執行環境。透過從 TypeScript 呼叫 Wasm,可以有效率地運用需要大量運算的流程,以及以 C/C++ 或 Rust 撰寫的現有原生函式庫。
基本執行流程
這裡我們將說明 Wasm 的基本執行流程。TypeScript(或瀏覽器)會取得 .wasm 檔案、將其實體化,並呼叫其匯出的函式。
-
- 使用 AssemblyScript、Rust、C++ 建立 .wasm 二進位檔,或直接準備現有的檔案。
-
- 在 TypeScript(或瀏覽器)中的抓取 .wasm 檔案,並同步或非同步地將其實體化。
-
- 呼叫匯出的函式,必要時可透過
WebAssembly.Memory共用記憶體。
- 呼叫匯出的函式,必要時可透過
WebAssembly.instantiateStreaming
接下來我們會展示如何載入 Wasm 檔並呼叫匯出的函式的基本範例。瀏覽器需要支援 instantiateStreaming。
以下的程式碼範例展示了從伺服器獲取 simple.wasm 並呼叫 add 函數的方法。
1// TypeScript: load-and-call.ts
2// Fetch and instantiate a wasm module and call its exported `add` function.
3async function run() {
4 const response = await fetch('http://localhost:3000/simple.wasm');
5 // Use instantiateStreaming when available for efficiency.
6 const { instance } = await WebAssembly.instantiateStreaming(response, {});
7 // @ts-ignore
8 const result = instance.exports.add(2, 3);
9 console.log('2 + 3 =', result);
10}
11run().catch(console.error);- Wasm 內部的函式會存於
instance.exports中。 - 由於 TypeScript 無法取得型別資訊,因此你需要使用
@ts-ignore或自行定義型別。
使用 AssemblyScript 的工作流程
AssemblyScript 可以用與 TypeScript 類似的語法撰寫 Wasm,非常適合 TypeScript 開發者使用。這裡我們準備了一個簡單的函數,將其編譯成 .wasm 和 .d.ts,並從 TypeScript 呼叫它。
1// assembly/index.ts (AssemblyScript)
2// npm install --save-dev assemblyscript
3
4// Export a simple function to add two integers.
5export function add(a: i32, b: i32): i32 {
6 return a + b;
7}- 透過使用
asc(AssemblyScript編譯器),你可以生成.wasm檔案,並且可以選擇產生型別定義檔.d.ts。如果要在本地測試,可以用 npm 安裝assemblyscript,然後進行編譯。
1# build commands
2# npm install --save-dev assemblyscript
3npx asc assembly/index.ts -o build/simple.wasm -t build/simple.wat --bindings esm --exportTable --sourceMap
4
5# optionally generate d.ts with --exportRuntime or use as-bind / loader tools以下是從 TypeScript 端進行抓取與呼叫的範例。
1// ts client that loads AssemblyScript-generated wasm
2async function runAssemblyScript() {
3 const res = await fetch('http://localhost:3000/build/simple.wasm');
4 const { instance } = await WebAssembly.instantiateStreaming(res, {});
5 // @ts-ignore
6 console.log('AssemblyScript add:', instance.exports.add(10, 7));
7}
8runAssemblyScript().catch(console.error);- AssemblyScript 雖然在記憶體模型與字串上需特別處理,但用於基本數值運算時非常簡單易用。
Rust + wasm-bindgen(強大且常用的選項)
本節說明如何使用 wasm-bindgen,將以 Rust 編寫的 Wasm 和 JavaScript 或 TypeScript 進行橋接的工作流程。這裡以簡單的 Fibonacci 函式作為示例,說明如何將產生的模組匯入為 ES 模組。
在 Rust 端使用 wasm-bindgen 匯出函式。
1// src/lib.rs (Rust)
2// install wasm-pack from https://drager.github.io/wasm-pack/installer/
3use wasm_bindgen::prelude::*;
4
5// Export a function to JavaScript using wasm-bindgen.
6#[wasm_bindgen]
7pub fn fib(n: u32) -> u32 {
8 if n <= 1 { return n; }
9 let mut a = 0;
10 let mut b = 1;
11 for _ in 2..=n {
12 let tmp = a + b;
13 a = b;
14 b = tmp;
15 }
16 b
17}- 使用
wasm-pack或wasm-bindgenCLI 編譯時,會自動產生 TypeScript 型別定義與 JS 包裝器,可直接以 ESM 匯入。
1# build with wasm-pack
2# install wasm-pack from https://drager.github.io/wasm-pack/installer/
3wasm-pack build --target nodejs --out-dir pkg在 TypeScript 端,從 pkg 匯入並使用 ES 模組。
1// Node.js: import WASM module built with --target web
2// import init, { fib } from '../pkg/my_wasm_module.js';
3// Node.js: import WASM module built with --target nodejs
4import wasm from '../pkg/my_wasm_module.js';
5
6async function run() {
7 //await init(); // --target web
8 console.log('fib(10)=', wasm.fib(10));
9}
10
11run().catch(console.error);wasm-pack會產生 JavaScript 包裝器與.d.ts型別定義檔,使其容易在 TypeScript 中使用。請注意,當你在wasm-pack指令中將--target選項設為web時,必須進行非同步初始化。
實際記憶體共享範例:傳遞與處理陣列(低階應用)
在與 Wasm 交換大量資料時,共享 ArrayBuffer 以提高資料交換效率非常重要。這裡以 AssemblyScript 作為範例,Rust 的 wasm-bindgen 也採用相同原理。
在 AssemblyScript 端,準備一個能寫入記憶體的匯出函式。例如對每個陣列元素進行平方的函式如下。
1// assembly/array_ops.ts (AssemblyScript)
2// Square values in place in the wasm linear memory starting at `ptr` for `len` elements.
3export function square_in_place(ptr: usize, len: i32): void {
4 // Treat memory as a pointer to 32-bit integers.
5 for (let i = 0; i < len; i++) {
6 let offset = ptr + (i << 2); // i * 4 bytes
7 let value = load<i32>(offset);
8 store<i32>(offset, value * value);
9 }
10}若要指定 AssemblyScript 的記憶體設定,請準備以下的 asconfig.json。
1{
2 "options": {
3 "memoryBase": 0,
4 "importMemory": false,
5 "initialMemory": 1,
6 "maximumMemory": 10
7 }
8}1 npx asc assembly/array_ops.ts -o build/array_ops.wasm -t build/array_ops.wat --bindings esm --exportTable --sourceMap- 呼叫此函式時,需要將
ArrayBuffer複製到 Wasm 記憶體空間,並傳遞指標。
以下是如何在 TypeScript 中利用 WebAssembly.Memory 複製資料並呼叫函式的範例。
1// TypeScript: use memory to pass array to wasm
2async function runArrayOps() {
3 const res = await fetch('http://localhost:3000/build/array_ops.wasm');
4 const { instance } = await WebAssembly.instantiateStreaming(res, {});
5 // @ts-ignore
6 const memory: WebAssembly.Memory = instance.exports.memory;
7 // Create a view into wasm memory.
8 const i32View = new Int32Array(memory.buffer);
9
10 // Example data
11 const input = new Int32Array([1, 2, 3, 4]);
12 // Choose an offset (in i32 elements) to copy data to (simple example: at index 0).
13 const offset = 0;
14 i32View.set(input, offset);
15
16 // Call wasm function: ptr in bytes, len in elements
17 // @ts-ignore
18 instance.exports.square_in_place(offset * 4, input.length);
19
20 // Read back result
21 const result = i32View.slice(offset, offset + input.length);
22 console.log('squared:', result);
23}
24runArrayOps().catch(console.error);memory.buffer是共用直線記憶體,盡量減少複製動作以提升處理速度。另外,指標是以位元組為單位的位置,而TypedArray則以元素數量管理,請小心區分勿混淆。
型別安全的處理方式:準備 TypeScript 型別定義
Wasm 匯出物件為 JavaScript 物件,因此在 TypeScript 端提供型別定義能讓開發更加便利。這裡提供一個簡單的型別定義檔範例。
以下是你可以手動建立的最簡型 simple.d.ts 型別定義。
1// simple.d.ts
2export function add(a: number, b: number): number;
3export const memory: WebAssembly.Memory;- 將此檔放入
tsconfig.json的typeRoots或使用declare module就能開啟型別檢查。wasm-pack會自動產生.d.ts型別定義檔,請善加利用。
執行時初始化模式:同步與非同步
由於 Wasm 模組需要 I/O(抓取)與編譯,通常會用非同步初始化方式。但也有一種方式是事先快取 WebAssembly.Module,再以同步方式實體化。
以下是非同步初始化 WebAssembly 的基礎程式結構。在實際專案中建議採用這種模式。
1// async init pattern
2async function initWasm(url: string) {
3 const res = await fetch(url);
4 const { instance, module } = await WebAssembly.instantiateStreaming(res, {});
5 return instance;
6}- 非同步初始化方便靈活地加入錯誤處理和延遲加載,實務開發中最為方便。此外,用
wasm-pack產生的程式會包含一個init()初始化 API,熟悉這種流程將有助於專案開發順利。
實用效能考量
以下是幾點需要注意的重要效能優化建議。在結合 TypeScript 與 WebAssembly 時,請參考下列最佳化要點。
- 如果函式呼叫非常頻繁,JavaScript 與 Wasm 之間的呼叫開銷可能會成為瓶頸。建議盡量將資料分批處理,一次完成多項運算。
- 記憶體配置與複製會增加處理負擔。善用共享緩衝區及指標,以減少這些操作。
- 處理浮點數時需要格外留意。在 TypeScript 中會被當作
number型別,建議在 Wasm 端對應型別以保證精度正確。
總結
透過結合 TypeScript 與 WebAssembly,可在瀏覽器中實現接近原生的效能。這在需要大量運算的任務,或想善用現有原生資產時特別有效。若想提升網頁應用效能,這種組合是非常強大的選擇。
您可以在我們的 YouTube 頻道上使用 Visual Studio Code 來跟隨上述文章一起學習。 請也查看我們的 YouTube 頻道。