WebAssembly 概述
简单来说,WebAssembly 是一种除了 HTML、JS 和 CSS 之外,可在浏览器环境下运行的一种二进制程序文件,文件格式为 wasm。
wasm 文件是一种低级的类汇编语言,运行效率接近原生性能,并可以和 JavaScript 进行交互。
AssemblyScript 概述
虽然 wasm 文件有各种优点,但是并不能使用 JavaScript 编写。通常情况下是使用诸如 C++或 Rust 等语言(语言支持情况)编写并生成 wasm 文件。
这对于前端开发人员来说还需要学习另外一门语言,存在上手难度。好在有 AssemblyScript。
AssemblyScript 是 TypeScript 的子集,包含了最基本的 JS 标准库。可使用有限的 TS 语法编写并生成 wasm 文件,对前端开发人员非常友好。
WebAssembly 关键概念
先看一个例子:
AssemblyScript
1 | declare function log(arg: i32): void; |
JavaScript
1 | (async () => { |
运行结果
1 | log from wasm 1003 |
模块
模块表示一个已经被浏览器编译过的 WebAssembly 二进制代码,可以被浏览器缓存和通过 postMessage 在 worker 中共享。一般一个 wasm 文件会被编译成一个模块。
注释 2 中的
module
就是一个 wasm 模块,表示 wasm 编译后的结果,可以被缓存。
实例
调用WebAssembly.Instance
传入 wasm 文件,会创建一个 WebAssembly 实例,类似 JS 中的 new 操作。一个实例包含了模块运行时的所有状态,包括导入导出值、内存和表格等。
注释 2 中的
instance
就是一个 wasm 实例,其中主要包含了模块导出的对象,可以直接被 JS 调用。
内存
内存是将 WebAssembly 运行的内存表示为 JS 中的 ArrayBuffer,可通过 TypedArray 对其进行读写。支持导入到 wasm 和从 wasm 导出。
注释 1 在 JS 创建了一段内存,将第一个元素设置为 123,并将其导入到 wasm 中使用。
表格
表格实际是引用值地址组成的数组,每一项表示在 wasm 中的引用类型,目前 WebAssembly 只支持函数类型。
注释 3 创建了一个表格,并将第 0 项设置为 wasm 中 add 函数的引用,可以在其后直接调用。
AssemblyScript 开发环境的搭建
- 新建一个项目,并用
yarn init
初始化 安装必要的依赖
1
2yarn add assemblyscript --dev
yarn add @assemblyscript/loader初始化项目目录结构和配置
1
yarn asinit .
确认初始化,并编译 wasm 后,目录结构如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17PROJECT_ROOT
├── asconfig.json
├── assembly
| ├── index.ts
| └── tsconfig.json
├── build
| ├── optimized.wasm
| ├── optimized.wasm.map
| ├── optimized.wat
| ├── untouched.wasm
| ├── untouched.wasm.map
| └── untouched.wat
├── index.js
├── package.json
├── tests
| └── index.js
└── yarn.lock编辑
assembly/index.ts
中的 ts 文件 (AssemblyScript)- 运行
yarn asbuild
会在 build 目录中生成 wasm 文件 可以通过修改并运行
node index.js
测试生成的 wasm,也可以在其他 node 项目中引用此项目直接使用 wasm1
2const myModule = require('path/to/mymodule');
myModule.add(1, 2);
AssemblyScript HelloWorld
使用 AssemblyScript 实现一个计算加法的程序,并在 wasm 内输出 log 到控制台
首先编写 wasm.ts 文件
注:AssemblyScript 是 TypeScript 的子集,并有一些书写上的限制,如没有 number 类型,需要用更具体的类型代替,如 i32, u32, f32, f64 等。
使用declare
声明从 JS 中导入的函数
使用export
语法导出函数到 JS
1 | declare function log(arg: string): void; |
编写 index.js 文件,并在 html 文件中使用 ESModule 方式引用
使用 @assemblyscript/loader
而不使用原生的方法是因为需要用到 loader 提供的 runtime(下文会作具体说明)
注:在编译 wasm 时需要提供 --exportRuntime
参数
1 | <script type="module" src="./index.js"></script> |
1 | import loader from 'https://cdn.jsdelivr.net/npm/@assemblyscript/loader/index.js'; |
输出结果:
1 | log from wasm: run add: 1, 2 |
在 AssemblyScript 中使用的 JS 函数可以通过loader.instantiate
的第二个参数,以文件名为 key 的对象中传入。
在 AssemblyScript 导出的 add 函数可以在loader.instantiate
返回的exports
中使用,除了自己定义的 add 函数外,loader 还导出了一些工具函数用来和 JS 进行数据转换。例子中用到了__getString
方法。
AssemblyScript 的导入和导出
导入
可以在任意文件声明导入的接口类型,然后从 JS 接收具体的函数。
例:新建一个模块,声明一个 log 函数接口,函数具体实现稍后会从 JS 中导入。
imports.ts
1 | export declare function log(arg: string): void; |
其他文件就可以通过如下方式引入导入的函数
wasm.ts
1 | import { log } from './imports'; |
log 函数需要在 JS 中通过导入对象的形式传入(key 为包含 declare 方法的文件名)
index.js
1 | const importObj = { |
导出
使用标准的 ESModule export 语法在入口文件处导出函数或常量
例:
wasm.ts
1 | export function add(a: i32, b: i32): i32 { |
AssemblyScript 与 JS 的数据转换
AssemblyScript 使用线性内存管理数据,底层只支持 number 类型 (i32, u8, f64 等),所以字符串、数组等类型不能直接导入导出或通过函数参数传入和返回。如需使用需要用到 AssemblyScript 的 loader (@assemblyscript/loader)。
通过loader.instantiate
的方式载入用 AssemblyScript 编译后的 wasm 后,会同时导出一些内置的工具函数,用来实现字符串或数组等类型的转换。
附带的工具函数 API
常用方法如下:
function __newString(str: string): number
: 创建字符串到指针值,用于向 wasm 传递字符串。function __getString(ptr: number): string
: 从 wasm 指针,读取字符串。function __newArray( id: number, values: valuesOrCapacity?: number[] | ArrayBufferView | number ): number
: 创建数组并传递到 wasmfunction __getArray(ptr: number): number[]
: 从 wasm 读取数组
字符串类型
AssemblyScript
1 | export function concat(a: string, b: string): string { |
JavaScript
1 | const { concat } = myModule.exports; |
数组类型
AssemblyScript
1 | export function sum(arr: Int32Array): i32 { |
JavaScript
1 | const { sum, Int32Array_ID } = myModule.exports; |
使用 unchecked
对提供的表达式不进行边界检查。
创建数组前需要在 AssemblyScript 中定义相关类型的 ID,并使用 ID 在 JS 中创建数组。
此外 AssemblyScript 还提供以下函数,获取类型化数组。
1 | function __getInt8ArrayView(ptr: number): Int8Array; |
自定义类
AssemblyScript 可以导出自定义类,类中可以包含数字和字符串类型的属性和自定义方法。
AssemblyScript
1 | export class Foo { |
JavaScript
1 | const { Foo, getFoo } = myModule.exports; |
使用 __pin
与 __unpin
防止 AssemblyScript 中的对象被回收。
可使用 as-bind 库简化数据转换操作:NPM
AssemblyScript 内存相关操作
AssemblyScript 支持和 JS 共享一段线性内存用来保存和处理数据。
注:在编译 wasm 时需要提供 --importMemory
参数
例:用操作内存的方法来实现一个加法
load
用于读取指定字节位置的数据store
用于将数据存入指定字节的位置
JavaScript
1 | (async () => { |
AssemblyScript
1 | // memory.grow(1); // 单位为页,1页为64KB |
AssemblyScript 相比于 TypeScript 存在的限制
- 需要指明更具体的 number 类型:
AssemblyScript Type | WebAssembly Type | Description |
---|---|---|
i32 | i32 | A 32-bit signed integer |
u32 | i32 | A 32-bit unsigned integer |
i64 | i64 | A 64-bit signed integer |
u64 | i64 | A 64-bit unsigned integer |
f32 | f32 | A 32-bit float |
f64 | f64 | A 64-bit float |
i8 | i32 | An 8-bit signed integer |
u8 | i32 | An 8-bit unsigned integer |
i16 | i32 | A 16-bit signed integer |
u16 | i32 | A 16-bit unsigned integer |
bool | i32 | A 1-bit unsigned integer |
由于是严格类型,所以
===
和==
是等价的所有类型必须是确定的,没有 any、undefined、联合类型
对象不能在运行时检查字段可用性
1
2
3
4
5
6
7
8
9
10
11
12
13function doSomething(foo: Foo): void {
if (foo.something) {
// ... some code ...
foo.something.length; // fails
}
}
function doSomething(foo: Foo): void {
let something = foo.something;
if (something) {
something.length; // works
}
}不支持
JSON
、RegExp
(存在社区解决方案)Date
支持有限 (仅包含 UTC 相关的实现)- 不支持函数闭包
- 不支持迭代器和扩展运算符
- 不支持
try...catch
和throw
异常处理 - 不支持 Promise
- JS 动态特性的相关功能
具体说明及其他限制详见官方文档
其他特性
- 支持一些装饰器函数,用来更改函数的行为或优化性能 官方文档
总结
由于 WebAssembly 不支持 JS 中的文档对象、网络、Promise 等,且数据类型支持有限,所以目前 WebAssembly 更适合对算法相关逻辑进行封装,通过缓存相关技术,可以将一个大的算法拆成几个小的 wasm,再通过 JS 粘到一起,兼顾高性能与灵活性。