百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

理解 wasm 基础概念(wdm的基本概念)

myzbx 2025-06-15 17:59 25 浏览

大家好,我是前端西瓜哥,这次带大家来简单系统学习一下 wasm(WebAssembly)。

示例源码在这个 github 仓库,可自行下载运行:

https://github.com/F-star/wasm-demo

wasm 是如何被加载运行的?

wasm 文件本身并不能像 JavaScript 一样,下载完成后就立即执行。

它更类似于 webgl 编译着色器代码,需要调用 JavaScript 提供的 API 去编译执行。

wasm 被加载并执行的过程一般为:

  1. 请求 wasm 文件;
  2. 转换为 ArrayBuffer 格式(也就是字节数组);
  3. 编译并返回 Module 对象(异步的,可使用阻塞写法);
  4. 基于 Module 创建一个 instance 实例(异步的,可使用阻塞写法) 。instance 的 exports 对象下为 wasm 暴露出来的方法和属性。创建 instance 有时需要提供一个额外的 importObject 对象,后文再细说。
  5. 执行 JavaScript 代码,调用 wasm 的方法,进行数据的交换。

代码实例:

fetch('./add.wasm')
  .then(rep => rep.arrayBuffer()) // 转 ArrayBuffer
  .then(bytes => WebAssembly.compile(bytes)) // 编译为 module 对象
  .then(module => WebAssembly.instantiate(module)) // 创建 instance 对象
  .then(instance => {
   // 拿到 wasm 文件暴露的 add 方法
    const { add } = instance.exports;
    console.log(add(12, 34));
  });

上面是为了让大家理解所有步骤,所以写得很繁琐。

我们有简单写法,用一个 API 把步骤 1、2、3、4 组合在一起:

WebAssembly.instantiateStreaming(fetch('./add.wasm')).then(res => {
  const { module, instance } = res;
  const { add } = instance.exports;
  console.log(add(12, 34));
});

WebAssembly.instantiateStreaming 支持流式编译,在 wasm 文件下载过程中就开始编译了,并最后会一次性返回编译和实例化产生的 module 和 instance 对象。

wasm 目前现在无法像 ES Module 一样,通过 import 的方式直接被引入(<script type="module">),将来会支持,且在提案中,但不会很快。

wat:wasm 文本格式

先写一个 wasm。

原来我打算用 C 写的,然后用 Emscripten 编译,但我发现编译出来的 wasm 有很多和 C 有关的冗余的代码,且需要配合生成好的代码量巨多的胶水 JavaScript 文件,有不少杂音。

为了更简单些,我选择写 wat,然后转为 wasm。

wat 指的是 wasm 的文本格式(WebAssembly text format)。wat 是一种低级语言,使用的是基于 S-表达式 的文本写法,可以直接映射为 WASM 的二进制指令,你可以把它类比为汇编语言。

因为用 wat 手写复杂逻辑并不可行,最后还是会用 C 或 Rust 这些高级语言去写业务。

所以这里我不会讲太多 wat 语法,目光更聚焦在 探究 wasm 是怎么和 js 通信的

要实现 wat 转 wasm,通常需要安装 WABT(The WebAssembly Binary Toolkit)工具集,用 wat2wasm 命令行工具进行转换。

如果觉得安装麻烦,可以用 WABT 提供的一个在线转换工具,贴 wat 文本上去点 download 按钮即可得到 wasm。

官方有提供 VSCode 插件,建议安装,可以高亮 wat 语法。

另外可以选中文件右键菜单可进行 wat 和 wasm 互转,但有点问题,一些正确的 wat 也会转换失败。

每次修改完都要手动生成 wasm 可能有点繁琐,可以考虑安装 wabt 命令工具,并配合 nodemon 监听 wat 文件,当文件被修改时自动编译 wasm。

数字类型

(module
  ;; 将两个 i32 类型的参数相加返回
  (func (export "add") (param i32 i32) (result i32)
    local.get 0
    local.get 1
    i32.add
  )
)

这里定义了一个 add 方法,接收两个 i32 类型的参数,相加并返回一个 i32 类型返回值。

wat 使用的栈式机器的方式执行的,将两个参数依次压入栈,然后调用相加运算,这个运算会取出栈顶的两个数进行相加,然后把结果压入栈。

最后函数会取栈顶的值作为返回值。

另外,目前 wasm 支持返回多个值了,JavaScript 那边会得到一个数组

;; 是行注释,另外 (;注释内容;) 是块注释。

wasm 的函数参数和返回值类型支持的数字类型有:i32、i64、f32、f64,分别代表 32 位和 64 位的整数和浮点数。(还有其他不常用的类型后面再讲)

生成 add.wasm 文件,然后再写一个 js 方法去加载调用 wasm 的方法:

WebAssembly.instantiateStreaming(fetch('./add.wasm')).then(res => {
  const { instance } = res;
  const { add } = instance.exports;
  console.log(add(100, 34));
  console.log(add(100.233, 34)); // 浮点数被 add 转成整数了
  console.log(add(false, 34)); // true 被转成 1,false 被转成 0
  // ...
});

查看控制台输出:

js 的数字只有一种类型:64 位浮点数,调用 wasm 函数会进行类型转换,在上面的例子中,add 方法会将其转为 32 位整数。

此外 js 的非数值类型也会转为数字,通常是 0 或 1,字符串的话会尝试转为数字(类似调用 Number())。

wasm 函数的返回值也会做类型转换为 js 的数字类型。如果返回的是 i64,在 JavaScript 会转换为 BigInt。

下面是另一种可读性更好的 wat 写法。这里给函数参数声明了名字,并给函数设置为变量,后面再导出(类似 js 的 export { add })。

(module
  ;; 将两个 i32 类型的参数相加返回
  (func $add (param $a i32) (param $b i32) (result i32)
    local.get $a
    local.get $b
    i32.add)
  (export "add" (func $add))
)

导入 JavaScript 方法

下面 wat 声明了需要导入的 JavaScript 方法 a.b()

(module
  ;; wasm 会拿到 importObject 的 a.b 方法
  (import "a" "b" (func $getNum (param i32)))
  (func (export "getNum")
    i32.const 114514
    call $getNum ;; 这里把数字传给了 importObject 的 a.b 方法
  )
)

导入的 js 方法需要声明名称和函数签名。

实例化 module 时提供前面提到的 importObject,去指定这个方法。

const importObject = {
  a: {
    b: (num) => {
      console.log('a.b', num) // 控制台输出:“a.b 114514”
    }
  }
}
WebAssembly.instantiateStreaming(fetch('./import.wasm'), importObject).then(res => {
  const { getNum } = res.instance.exports;
  getNum();
});

调用 wasm 定义的 getNum 方法时,该方法会调用 js 声明的 a.b() 方法,并传入一个整数。

a 是模块名,b 是这个模块的一个属性,模块属性除了可以是函数,也可以是其他的类型,比如线性内存 memory、表格 table。

我们写 C 编译成 wasm,其中的 printf 能够在控制台打印出来,就是调用了导入的 js 的胶水方法,把一些二进制数据转换成 js 字符串,然后调用 console.log() 输出。

全局变量

将从 importObject.js.global 传过来的变量作为 wasm 的全局变量。

定义了两个方法:

  1. getGlobal:返回这个全局变量;
  2. incGlobal:给全局变量 + 1。
(module
  (global $g (import "js" "global") (mut i32))
  (func (export "getGlobal") (result i32)
    (global.get $g)
  )
  (func (export "incGlobal")
    (global.set $g
      (
        i32.add
        (global.get $g)
        (i32.const 1)
      )
    )
  )
)

js 中用 new WebAssembly.Global() 创建 global 对象然后导入。

const importObject = {
  js: {
    // 一个初始值为 233 的 i32 变量
    global: new WebAssembly.Global(
      {
        value: 'i32',
        mutable: true,
      },
      233
    ),
  },
};
WebAssembly.instantiateStreaming(fetch('./global.wasm'), importObject).then(
  (res) => {
    const { instance } = res;
    console.log(instance);
    const { getGlobal, incGlobal } = res.instance.exports;
    console.log('全局变量');
    console.log(getGlobal()); // 输出:233
    incGlobal();
    incGlobal();
    console.log(getGlobal()); // 输出:235
  }
);

也可以在 js 中直接用 importObject.js.global.value 拿到全局变量的值。

也可以在 wasm 中定义 global 变量,global 变量可以定义多个。

(global $g2 (mut i32) (i32.const 99))

复杂变量类型

wasm 的函数无法接收和返回一些复杂的高级类型,比如字符串、对象,这时候就需要用到 线性内存(memory) 了。

线性内存需要用到 WebAssembly.Memory 对象,这个对象是 ArrayBuffer。

js 和 wasm 共享这个 ArrayBuffer,作为传输媒介,然后双方都在各自的作用域进行序列和反序列化。

这也是 wasm 让人诟病的通信问题:

如果计算本身的 CPU 密集度不高,那瓶颈就落到数据序列化反序列化以及通信上了,别说提升性能了,降低性能都可能

wat:

(module
  (import "console" "log" (func $log (param i32 i32)))
  ;; 传入的 memory 大小为 1 页
  (import "js" "mem" (memory 1))
  ;; 在 memory 的地址 0 处设置数据 "Hi"
  (data (i32.const 0) "Hi")
  
  (func (export "writeHi")
    i32.const 0  ;; 字符串起始位置
    i32.const 2  ;; 字符串长度
    call $log
  )
)

js:

// memory 对象,大小为 1 页(page),1 页为 64 KB
const memory = new WebAssembly.Memory({ initial: 1 });
// wasm 无法直接返回字符串,但可以修改线性内存
// 然后再指定线性内存的区间让 js 去截取需要的 ArrayBuffer
// 最后 ArrayBuffer 转 字符串
function consoleLogString(offset, length) {
  const bytes = new Uint8Array(memory.buffer, offset, length);
  const string = new TextDecoder('utf-8').decode(bytes);
  console.log(string);
}
const importObject = {
  console: { log: consoleLogString },
  js: { mem: memory },
};
WebAssembly.instantiateStreaming(fetch('./memory.wasm'), importObject).then(
  (res) => {
    res.instance.exports.writeHi();
  }
);

也可以在 js 传字符串给 wasm,但 js 这边要做字符串转 ArrayBuffer 的操作

下面是拼接两个字符串返回新字符串示例。

wat:

(module
  (import "console" "log" (func $log (param i32 i32)))
  (import "js" "mem" (memory 1))
  ;; 函数接受两个字符串并拼接它们
  (func $concatStrings (param $offset1 i32) (param $length1 i32) (param $offset2 i32) (param $length2 i32) (result i32) (result i32)
    ;; 这里的代码是将两个字符串拼接到内存中,并返回新字符串的偏移量和长度
    ;; 注意:为了简单起见,这里假设你有足够的内存空间来拼接字符串
    (local $newOffset i32)
    ;; 假设新的偏移量是在第一个字符串的结束处
    local.get $offset1
    local.get $length1
    i32.add
    local.set $newOffset
    ;; 将第二个字符串拷贝到新的偏移量处
    local.get $newOffset
    local.get $offset2
    local.get $length2
    memory.copy
    ;; 返回新的偏移量和长度
    local.get $offset1
    local.get $length1
    local.get $length2
    i32.add
  )
  (func (export "concatAndLog") (param $offset1 i32) (param $length1 i32) (param $offset2 i32) (param $length2 i32)
    ;; 调用上面的拼接函数
    local.get $offset1
    local.get $length1
    local.get $offset2
    local.get $length2
    call $concatStrings
    ;; 使用结果来调用$log
    call $log
  )
)

js:

const memory = new WebAssembly.Memory({ initial: 1 });
function consoleLogString(offset, length) {
  // console.log(offset, length);
  const bytes = new Uint8Array(memory.buffer, offset, length);
  const string = new TextDecoder('utf-8').decode(bytes);
  console.log(string); // 输出 Hello, WebAssembly!
}
let currentOffset = 0; // 添加这个变量来跟踪当前可用的内存偏移量
function stringToMemory(str, mem) {
  const encoder = new TextEncoder();
  const bytes = encoder.encode(str);
  new Uint8Array(mem.buffer, currentOffset, bytes.length).set(bytes);
  const returnOffset = currentOffset;
  currentOffset += bytes.length; // 更新偏移量
  return { offset: returnOffset, length: bytes.length };
}
const importObject = {
  console: { log: consoleLogString },
  js: { mem: memory },
};
WebAssembly.instantiateStreaming(fetch('./concat.wasm'), importObject).then(
  (res) => {
    const str1 = 'Hello, ';
    const str2 = 'WebAssembly!';
    const mem1 = stringToMemory(str1, memory);
    const mem2 = stringToMemory(str2, memory);
    res.instance.exports.concatAndLog(
      mem1.offset,
      mem1.length,
      mem2.offset,
      mem2.length
    );
  }
);

其他类型也一样的思路,只要支持转换成 ArrayBuffer,然后转换回来就好了。

一个 wasm 模块只能定义一个线性内存 memory,这个是出于简单的考量。

表格 table

table 是一个大小可变的引用数组,指向 wasm 的代码地址。

前面的 wat 执行代码时,会使用 run 指令接一个 静态 的函数索引。但有时候函数索引需要是动态,一会指向函数 a,过一段时间又指向 b。

这时候我们就可以使用 table 去维护。

(table 2 funcref)

anyfunc 类型,代表可以是任何签名的函数引用。

因为安全问题函数引用不能保存在线性内存(memory)中。因为线性内存保存地址没意义,而存真正的函数数据源有可能被恶意修改,有安全问题。

所以整出了这么一个抽象的 table 数组,这个 table 无法被读取真正的内容,只能更新一下数组的引用。

下面是一个示例,在 wat 创建了一个 table,然后让 js 根据索引调用 table 中的动态引用的函数。

wat

(module
  ;; table 大小为 2,且为函数引用类型。
  (table $t 2 funcref)
  ;; table 从 0 偏移值填充声明的两个函数
  ;; 0 指向 $f1,1 指向 $f2
  (elem (i32.const 0) $f1 $f2)
  ;; 函数声明可以在任何位置
  (func $f1 (result i32)
    i32.const 22
  )
  (func $f2 (result i32)
    i32.const 33
  )
  ;; 定义函数类型,一个返回 i32 的函数(类比 ts 的函数类型)
  (type $return_i32 (func (result i32)))
  ;; 暴露一个 callByIndex 方法给 js
  ;; callByIndex(0) 表示调用 table 上索引为 0 的函数。
  (func (export "callByIndex") (param $i i32) (result i32)
    ;; (间接)调用 $i 索引值在 table 中指向的方法
    call_indirect (type $return_i32)
  )
)

js

WebAssembly.instantiateStreaming(fetch('./table.wasm')).then((res) => {
  const { callByIndex } = res.instance.exports;
  console.log(callByIndex(0)); // 22
  console.log(callByIndex(1)); // 33
});

也可以在 js 中更新 table,让一些索引指向新的函数。

但需要注意,这个函数需要时 wasm 导出,而不是 js 函数。

下面是对应的示例。

wat:

(module
   ;; 导入 table
  (import "js" "table" (table 1 funcref))
  (elem (i32.const 0) $f1)
  (func $f1 (result i32)
    i32.const 22
  )
  (type $return_i32 (func (result i32)))
  (func (export "call") (result i32)
    i32.const 0
    call_indirect (type $return_i32)
  )
  (func (export "get666") (result i32)
    i32.const 666
  )
)

js:

const table = new WebAssembly.Table({ initial: 1, element: 'anyfunc' });
const importObject = {
  js: { table },
};
WebAssembly.instantiateStreaming(
  fetch('./outer-table.wasm'),
  importObject
).then((res) => {
  const { call, get666 } = res.instance.exports;
  console.log(call()); // 22
  console.log(table.get(0)); // 获取 wasm 函数
  table.set(0, get666); // 更换 table[0] 的函数。
  console.log(call()); // 666
});

在 wat 中,anyfunc 是旧写法,现在换成了 funcref,来表示函数引用。

不过 js 中创建 table,element 参数还得传 "anyfunc"。

table 的这个特性可以实现类似 dll 的动态链接能力,可以在程序运行时才动态链接需要的代码和数据。

引用类型

wasm 的函数现在支持传 引用类型(externref)

(func (export "callJSFunction") (param externref)
  ...
)

你可以传任何 js 变量作为 externref 类型传入 wasm 函数,但该变量在 wasm 不能被读写和执行,但可以把作为返回值,或是它作为参数传给 import 进来的 js 函数。

wasm 只能对 externref 做中转,传入以及返回回去,无法做任何其他操作

示例:

(module
  (type $jsFunc (func (param externref)))
  (func $invoke (import "js" "invokeFunction") (type $jsFunc))
  
  (func (export "callJSFunction") (param externref)
    local.get 0
    call $invoke
  )
)
const importObject = {
  js: {
    invokeFunction: (fn) => {
      fn();
    },
  },
};
WebAssembly.instantiateStreaming(fetch('./type.wasm'), importObject).then(
  (res) => {
    const { instance } = res;
    const { callJSFunction } = instance.exports;
    callJSFunction(() => {
      console.log('被执行的是来自 js 函数');
    });
  }
);

矢量类型

v128,一个 128 比特的矢量类型。

用于 SIMD(Single Instruction, Multiple Data),它是一种计算机并行处理技术,允许一个单一的操作指令同时处理多个数据元素,使用用在大量数据执行相同操作的场景,比如矩阵运算。

v128 是其他数据的打包,打包一起好做并行运行,提高计算速度。

这些数据可能是:

  1. 4 个 i32(或 f32)
  2. 2 个 i64(或 f64)
  3. 16 个 i8
  4. 8 个 i16

然后它们会使用类似 i32x4 的指令进行批量操作:

i32x4.add (local.get $a) (local.get $b)

虽然没有 i8 和 i16 这种类型,但它们本质是 ArrayBuffer(字节数组)的一种高层级,js 那边可以用 ArrayBuffer 构造出 Int8Array 对象。

所以 wat 提供了对应的指令,比如 i8x16.add

示例

(module
  (memory 1)
  (export "memory" (memory 0))
  (func (export "add_vectors")
    (param $aOffset i32) (param $bOffset i32)
    (local $a v128) (local $b v128)
    (local.set $a (v128.load (local.get $aOffset)))
    (local.set $b (v128.load (local.get $bOffset)))
    (v128.store (i32.const 0) (i32x4.add (local.get $a) (local.get $b)))
  )
)
WebAssembly.instantiateStreaming(fetch('./v128.wasm')).then((res) => {
  const { add_vectors, memory } = res.instance.exports;
  // 首先在内存中分配两个向量a和b
  const a = new Int32Array(memory.buffer, 0, 4);
  const b = new Int32Array(memory.buffer, 16, 4);
  // 初始化向量a和b的值
  a.set([1, 2, 3, 4]);
  b.set([5, 6, 7, 8]);
  console.log('Vector A:', a);
  console.log('Vector B:', b);
  // 调用add_vectors函数,传入向量a和b在内存中的偏移量
  add_vectors(0, 16);
  // 读取和打印结果
  const result = new Int32Array(memory.buffer, 0, 4);
  console.log('Result:', result); // [6, 8, 10, 12]
});

多线程


wasm 支持多线程。

我们可以使用多个 Web Worker 各自创建 wasm 实例,让它们共享同一段内存 SharedArrayBuffer。

因为多线程特有的竞态条件问题,我们需要用到 Atomics 对象,它提供了一些原子操作,防止冲突。

最后是 wait/notify 进行线程的挂起和唤醒。

这个用的不多,就简单介绍一下就好了。

结尾

wasm 是 js 的一个强有力的补充,未来可期,在一些领域比如图像处理、音视频处理大有可为。

但也不得不承认 wasm 并不能很简单地就能给应用提高性能的,因为安全原因,相比原生是有一定性能损失的。

如果没做正确的设计甚至因为通信成本导致负优化,你需要考量性能的瓶颈在哪里,到底是代码写得烂呢还是 CPU 计算就是高。v8 的 JIT 过于优秀,导致 wasm 的光芒不够耀眼。

另外,wasm 有不小的学习成本的。

但不可否认,wasm 是前端的一个大方向,还是有一定学习投入的必要。

我是前端西瓜哥,欢迎关注我,学习更多 wasm 知识。

相关推荐

如何设计一个优秀的电子商务产品详情页

加入人人都是产品经理【起点学院】产品经理实战训练营,BAT产品总监手把手带你学产品电子商务网站的产品详情页面无疑是设计师和开发人员关注的最重要的网页之一。产品详情页面是客户作出“加入购物车”决定的页面...

怎么在JS中使用Ajax进行异步请求?

大家好,今天我来分享一项JavaScript的实战技巧,即如何在JS中使用Ajax进行异步请求,让你的网页速度瞬间提升。Ajax是一种在不刷新整个网页的情况下与服务器进行数据交互的技术,可以实现异步加...

中小企业如何组建,管理团队_中小企业应当如何开展组织结构设计变革

前言写了太多关于产品的东西觉得应该换换口味.从码农到架构师,从前端到平面再到UI、UE,最后走向了产品这条不归路,其实以前一直再给你们讲.产品经理跟项目经理区别没有特别大,两个岗位之间有很...

前端监控 SDK 开发分享_前端监控系统 开源

一、前言随着前端的发展和被重视,慢慢的行业内对于前端监控系统的重视程度也在增加。这里不对为什么需要监控再做解释。那我们先直接说说需求。对于中小型公司来说,可以直接使用三方的监控,比如自己搭建一套免费的...

Ajax 会被 fetch 取代吗?Axios 怎么办?

大家好,很高兴又见面了,我是"高级前端进阶",由我带着大家一起关注前端前沿、深入前端底层技术,大家一起进步,也欢迎大家关注、点赞、收藏、转发!今天给大家带来的主题是ajax、fetch...

前端面试题《AJAX》_前端面试ajax考点汇总

1.什么是ajax?ajax作用是什么?AJAX=异步JavaScript和XML。AJAX是一种用于创建快速动态网页的技术。通过在后台与服务器进行少量数据交换,AJAX可以使网页实...

Ajax 详细介绍_ajax

1、ajax是什么?asynchronousjavascriptandxml:异步的javascript和xml。ajax是用来改善用户体验的一种技术,其本质是利用浏览器内置的一个特殊的...

6款可替代dreamweaver的工具_替代powerdesigner的工具

dreamweaver对一个web前端工作者来说,再熟悉不过了,像我07年接触web前端开发就是用的dreamweaver,一直用到现在,身边的朋友有跟我推荐过各种更好用的可替代dreamweaver...

我敢保证,全网没有再比这更详细的Java知识点总结了,送你啊

接下来你看到的将是全网最详细的Java知识点总结,全文分为三大部分:Java基础、Java框架、Java+云数据小编将为大家仔细讲解每大部分里面的详细知识点,别眨眼,从小白到大佬、零基础到精通,你绝...

福斯《死侍》发布新剧照 &quot;小贱贱&quot;韦德被改造前造型曝光

时光网讯福斯出品的科幻片《死侍》今天发布新剧照,其中一张是较为罕见的死侍在被改造之前的剧照,其余两张剧照都是死侍在执行任务中的状态。据外媒推测,片方此时发布剧照,预计是为了给不久之后影片发布首款正式预...

2021年超详细的java学习路线总结—纯干货分享

本文整理了java开发的学习路线和相关的学习资源,非常适合零基础入门java的同学,希望大家在学习的时候,能够节省时间。纯干货,良心推荐!第一阶段:Java基础重点知识点:数据类型、核心语法、面向对象...

不用海淘,真黑五来到你身边:亚马逊15件热卖爆款推荐!

Fujifilm富士instaxMini8小黄人拍立得相机(黄色/蓝色)扫二维码进入购物页面黑五是入手一个轻巧可爱的拍立得相机的好时机,此款是mini8的小黄人特别版,除了颜色涂装成小黄人...

2025 年 Python 爬虫四大前沿技术:从异步到 AI

作为互联网大厂的后端Python爬虫开发,你是否也曾遇到过这些痛点:面对海量目标URL,单线程爬虫爬取一周还没完成任务;动态渲染的SPA页面,requests库返回的全是空白代码;好不容易...

最贱超级英雄《死侍》来了!_死侍超燃

死侍Deadpool(2016)导演:蒂姆·米勒编剧:略特·里斯/保罗·沃尼克主演:瑞恩·雷诺兹/莫蕾娜·巴卡林/吉娜·卡拉诺/艾德·斯克林/T·J·米勒类型:动作/...

停止javascript的ajax请求,取消axios请求,取消reactfetch请求

一、Ajax原生里可以通过XMLHttpRequest对象上的abort方法来中断ajax。注意abort方法不能阻止向服务器发送请求,只能停止当前ajax请求。停止javascript的ajax请求...