TypeScript中的WebAssembly

TypeScript中的WebAssembly

本文讲解了TypeScript中的WebAssembly。

我们将讲解TypeScript与WebAssembly集成的实用且易于理解的方法。

YouTube Video

TypeScript中的WebAssembly

WebAssembly(简称Wasm)是一种运行时二进制格式,可在浏览器中以接近原生的速度运行。通过在TypeScript中调用Wasm,您可以高效利用计算密集型流程以及用C/C++或Rust编写的现有原生库。

基本执行流程

这里我们将讲解Wasm的基本执行流程。TypeScript(或浏览器)获取.wasm文件,对其进行实例化,并调用导出的函数。

    1. 使用AssemblyScript、Rust、C++等生成.wasm二进制文件,或准备已有的文件。
    1. 在TypeScript(或浏览器)中获取.wasm文件,并同步或异步地实例化。
    1. 调用导出的函数,并在需要时通过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}
  • 通过使用 ascAssemblyScript 编译器),你可以生成一个 .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-packwasm-bindgen CLI构建时,会自动生成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.jsontypeRoots中或用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频道。

YouTube Video