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 进行连接的工作流程。此处以一个简单的斐波那契数列函数为例,演示如何将生成的模块作为ES模块导入。
通过wasm-bindgen在Rust端导出函数。
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结合,可在浏览器中实现接近原生的性能。这在进行计算密集型任务或需要利用现有原生库时特别有效。当您想提升Web应用性能时,这种组合是一个非常强大的选择。
您可以在我们的YouTube频道上使用Visual Studio Code跟随上述文章进行学习。 请也查看我们的YouTube频道。