理解 Wasm 基础概念,了解 Wasm 是如何被加载运行的?
2023-12-06 09:19:05 软件 244观看
摘要大家好,我是前端西瓜哥,这次带大家来简单系统学习一下 wasm(WebAssembly)。示例源码在这个 github 仓库,可自行下载运行:https://github.com/F-star/wasm-demo。wasm 是如何被加载运行的?wasm 文件本身并不能像 JavaScript

Wxq28资讯网——每日最新资讯28at.com

大家好,我是前端西瓜哥,这次带大家来简单系统学习一下 wasm(WebAssembly)。Wxq28资讯网——每日最新资讯28at.com

示例源码在这个 github 仓库,可自行下载运行:Wxq28资讯网——每日最新资讯28at.com

https://github.com/F-star/wasm-demo。Wxq28资讯网——每日最新资讯28at.com

wasm 是如何被加载运行的?

wasm 文件本身并不能像 JavaScript 一样,下载完成后就立即执行。Wxq28资讯网——每日最新资讯28at.com

它更类似于 webgl 编译着色器代码,需要调用 JavaScript 提供的 API 去编译执行。Wxq28资讯网——每日最新资讯28at.com

wasm 被加载并执行的过程一般为:Wxq28资讯网——每日最新资讯28at.com

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

代码实例:Wxq28资讯网——每日最新资讯28at.com

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));  });

上面是为了让大家理解所有步骤,所以写得很繁琐。Wxq28资讯网——每日最新资讯28at.com

我们有简单写法,用一个 API 把步骤 1、2、3、4 组合在一起:Wxq28资讯网——每日最新资讯28at.com

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 对象。Wxq28资讯网——每日最新资讯28at.com

Wxq28资讯网——每日最新资讯28at.com

wasm 目前现在无法像 ES Module 一样,通过 import 的方式直接被引入(<script type="module">),将来会支持,且在提案中,但不会很快。Wxq28资讯网——每日最新资讯28at.com

wat:wasm 文本格式

先写一个 wasm。Wxq28资讯网——每日最新资讯28at.com

原来我打算用 C 写的,然后用 Emscripten 编译,但我发现编译出来的 wasm 有很多和 C 有关的冗余的代码,且需要配合生成好的代码量巨多的胶水 JavaScript 文件,有不少杂音。Wxq28资讯网——每日最新资讯28at.com

为了更简单些,我选择写 wat,然后转为 wasm。Wxq28资讯网——每日最新资讯28at.com

wat 指的是 wasm 的文本格式(WebAssembly text format)。wat 是一种低级语言,使用的是基于 S-表达式 的文本写法,可以直接映射为 WASM 的二进制指令,你可以把它类比为汇编语言。Wxq28资讯网——每日最新资讯28at.com

因为用 wat 手写复杂逻辑并不可行,最后还是会用 C 或 Rust 这些高级语言去写业务。Wxq28资讯网——每日最新资讯28at.com

所以这里我不会讲太多 wat 语法,目光更聚焦在 探究 wasm 是怎么和 js 通信的。Wxq28资讯网——每日最新资讯28at.com

要实现 wat 转 wasm,通常需要安装 WABT(The WebAssembly Binary Toolkit)工具集,用 wat2wasm 命令行工具进行转换。Wxq28资讯网——每日最新资讯28at.com

如果觉得安装麻烦,可以用 WABT 提供的一个在线转换工具,贴 wat 文本上去点 download 按钮即可得到 wasm。Wxq28资讯网——每日最新资讯28at.com

Wxq28资讯网——每日最新资讯28at.com

官方有提供 VSCode 插件,建议安装,可以高亮 wat 语法。Wxq28资讯网——每日最新资讯28at.com

另外可以选中文件右键菜单可进行 wat 和 wasm 互转,但有点问题,一些正确的 wat 也会转换失败。Wxq28资讯网——每日最新资讯28at.com

Wxq28资讯网——每日最新资讯28at.com

每次修改完都要手动生成 wasm 可能有点繁琐,可以考虑安装 wabt 命令工具,并配合 nodemon 监听 wat 文件,当文件被修改时自动编译 wasm。Wxq28资讯网——每日最新资讯28at.com

数字类型

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

这里定义了一个 add 方法,接收两个 i32 类型的参数,相加并返回一个 i32 类型返回值。Wxq28资讯网——每日最新资讯28at.com

wat 使用的栈式机器的方式执行的,将两个参数依次压入栈,然后调用相加运算,这个运算会取出栈顶的两个数进行相加,然后把结果压入栈。Wxq28资讯网——每日最新资讯28at.com

最后函数会取栈顶的值作为返回值。Wxq28资讯网——每日最新资讯28at.com

另外,目前 wasm 支持返回多个值了,JavaScript 那边会得到一个数组。Wxq28资讯网——每日最新资讯28at.com

;; 是行注释,另外 (;注释内容;) 是块注释。Wxq28资讯网——每日最新资讯28at.com

wasm 的函数参数和返回值类型支持的数字类型有:i32、i64、f32、f64,分别代表 32 位和 64 位的整数和浮点数。(还有其他不常用的类型后面再讲)Wxq28资讯网——每日最新资讯28at.com

生成 add.wasm 文件,然后再写一个 js 方法去加载调用 wasm 的方法:Wxq28资讯网——每日最新资讯28at.com

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  // ...});

查看控制台输出:Wxq28资讯网——每日最新资讯28at.com

Wxq28资讯网——每日最新资讯28at.com

js 的数字只有一种类型:64 位浮点数,调用 wasm 函数会进行类型转换,在上面的例子中,add 方法会将其转为 32 位整数。Wxq28资讯网——每日最新资讯28at.com

此外 js 的非数值类型也会转为数字,通常是 0 或 1,字符串的话会尝试转为数字(类似调用 Number())。Wxq28资讯网——每日最新资讯28at.com

wasm 函数的返回值也会做类型转换为 js 的数字类型。如果返回的是 i64,在 JavaScript 会转换为 BigInt。Wxq28资讯网——每日最新资讯28at.com

下面是另一种可读性更好的 wat 写法。这里给函数参数声明了名字,并给函数设置为变量,后面再导出(类似 js 的 export { add })。Wxq28资讯网——每日最新资讯28at.com

(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()。Wxq28资讯网——每日最新资讯28at.com

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

导入的 js 方法需要声明名称和函数签名。Wxq28资讯网——每日最新资讯28at.com

实例化 module 时提供前面提到的 importObject,去指定这个方法。Wxq28资讯网——每日最新资讯28at.com

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() 方法,并传入一个整数。Wxq28资讯网——每日最新资讯28at.com

a 是模块名,b 是这个模块的一个属性,模块属性除了可以是函数,也可以是其他的类型,比如线性内存 memory、表格 table。Wxq28资讯网——每日最新资讯28at.com

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

全局变量

将从 importObject.js.global 传过来的变量作为 wasm 的全局变量。Wxq28资讯网——每日最新资讯28at.com

定义了两个方法:Wxq28资讯网——每日最新资讯28at.com

  • getGlobal:返回这个全局变量;
  • 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 对象然后导入。Wxq28资讯网——每日最新资讯28at.com

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 拿到全局变量的值。Wxq28资讯网——每日最新资讯28at.com

也可以在 wasm 中定义 global 变量,global 变量可以定义多个。Wxq28资讯网——每日最新资讯28at.com

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

复杂变量类型

wasm 的函数无法接收和返回一些复杂的高级类型,比如字符串、对象,这时候就需要用到 线性内存(memory) 了。Wxq28资讯网——每日最新资讯28at.com

线性内存需要用到 WebAssembly.Memory 对象,这个对象是 ArrayBuffer。Wxq28资讯网——每日最新资讯28at.com

Wxq28资讯网——每日最新资讯28at.com

js 和 wasm 共享这个 ArrayBuffer,作为传输媒介,然后双方都在各自的作用域进行序列和反序列化。Wxq28资讯网——每日最新资讯28at.com

这也是 wasm 让人诟病的通信问题:Wxq28资讯网——每日最新资讯28at.com

如果计算本身的 CPU 密集度不高,那瓶颈就落到数据序列化反序列化以及通信上了,别说提升性能了,降低性能都可能。Wxq28资讯网——每日最新资讯28at.com

wat:Wxq28资讯网——每日最新资讯28at.com

(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:Wxq28资讯网——每日最新资讯28at.com

// memory 对象,大小为 1 页(page),1 页为 64 KBconst 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 的操作。Wxq28资讯网——每日最新资讯28at.com

下面是拼接两个字符串返回新字符串示例。Wxq28资讯网——每日最新资讯28at.com

wat:Wxq28资讯网——每日最新资讯28at.com

(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:Wxq28资讯网——每日最新资讯28at.com

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,然后转换回来就好了。Wxq28资讯网——每日最新资讯28at.com

一个 wasm 模块只能定义一个线性内存 memory,这个是出于简单的考量。Wxq28资讯网——每日最新资讯28at.com

表格 table

table 是一个大小可变的引用数组,指向 wasm 的代码地址。Wxq28资讯网——每日最新资讯28at.com

前面的 wat 执行代码时,会使用 run 指令接一个 静态 的函数索引。但有时候函数索引需要是动态,一会指向函数 a,过一段时间又指向 b。Wxq28资讯网——每日最新资讯28at.com

这时候我们就可以使用 table 去维护。Wxq28资讯网——每日最新资讯28at.com

(table 2 funcref)

anyfunc 类型,代表可以是任何签名的函数引用。Wxq28资讯网——每日最新资讯28at.com

因为安全问题函数引用不能保存在线性内存(memory)中。因为线性内存保存地址没意义,而存真正的函数数据源有可能被恶意修改,有安全问题。Wxq28资讯网——每日最新资讯28at.com

所以整出了这么一个抽象的 table 数组,这个 table 无法被读取真正的内容,只能更新一下数组的引用。Wxq28资讯网——每日最新资讯28at.com

下面是一个示例,在 wat 创建了一个 table,然后让 js 根据索引调用 table 中的动态引用的函数。Wxq28资讯网——每日最新资讯28at.com

watWxq28资讯网——每日最新资讯28at.com

(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:Wxq28资讯网——每日最新资讯28at.com

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

也可以在 js 中更新 table,让一些索引指向新的函数。Wxq28资讯网——每日最新资讯28at.com

但需要注意,这个函数需要时 wasm 导出,而不是 js 函数。Wxq28资讯网——每日最新资讯28at.com

下面是对应的示例。Wxq28资讯网——每日最新资讯28at.com

wat:Wxq28资讯网——每日最新资讯28at.com

(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:Wxq28资讯网——每日最新资讯28at.com

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,来表示函数引用。Wxq28资讯网——每日最新资讯28at.com

不过 js 中创建 table,element 参数还得传 "anyfunc"。Wxq28资讯网——每日最新资讯28at.com

table 的这个特性可以实现类似 dll 的动态链接能力,可以在程序运行时才动态链接需要的代码和数据。Wxq28资讯网——每日最新资讯28at.com

引用类型

wasm 的函数现在支持传 引用类型(externref)。Wxq28资讯网——每日最新资讯28at.com

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

你可以传任何 js 变量作为 externref 类型传入 wasm 函数,但该变量在 wasm 不能被读写和执行,但可以把作为返回值,或是它作为参数传给 import 进来的 js 函数。Wxq28资讯网——每日最新资讯28at.com

wasm 只能对 externref 做中转,传入以及返回回去,无法做任何其他操作。Wxq28资讯网——每日最新资讯28at.com

示例:Wxq28资讯网——每日最新资讯28at.com

(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 比特的矢量类型。Wxq28资讯网——每日最新资讯28at.com

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

v128 是其他数据的打包,打包一起好做并行运行,提高计算速度。Wxq28资讯网——每日最新资讯28at.com

这些数据可能是:Wxq28资讯网——每日最新资讯28at.com

  • 4 个 i32(或 f32)
  • 2 个 i64(或 f64)
  • 16 个 i8
  • 8 个 i16

然后它们会使用类似 i32x4 的指令进行批量操作:Wxq28资讯网——每日最新资讯28at.com

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

虽然没有 i8 和 i16 这种类型,但它们本质是 ArrayBuffer(字节数组)的一种高层级,js 那边可以用 ArrayBuffer 构造出 Int8Array 对象。Wxq28资讯网——每日最新资讯28at.com

所以 wat 提供了对应的指令,比如 i8x16.add。Wxq28资讯网——每日最新资讯28at.com

示例:Wxq28资讯网——每日最新资讯28at.com

(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]});

Wxq28资讯网——每日最新资讯28at.com

多线程

wasm 支持多线程。Wxq28资讯网——每日最新资讯28at.com

我们可以使用多个 Web Worker 各自创建 wasm 实例,让它们共享同一段内存 SharedArrayBuffer。Wxq28资讯网——每日最新资讯28at.com

因为多线程特有的竞态条件问题,我们需要用到 Atomics 对象,它提供了一些原子操作,防止冲突。Wxq28资讯网——每日最新资讯28at.com

最后是 wait/notify 进行线程的挂起和唤醒。Wxq28资讯网——每日最新资讯28at.com

这个用的不多,就简单介绍一下就好了。Wxq28资讯网——每日最新资讯28at.com

结尾

wasm 是 js 的一个强有力的补充,未来可期,在一些领域比如图像处理、音视频处理大有可为。Wxq28资讯网——每日最新资讯28at.com

但也不得不承认 wasm 并不能很简单地就能给应用提高性能的,因为安全原因,相比原生是有一定性能损失的。Wxq28资讯网——每日最新资讯28at.com

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

另外,wasm 有不小的学习成本的。Wxq28资讯网——每日最新资讯28at.com

但不可否认,wasm 是前端的一个大方向,还是有一定学习投入的必要。Wxq28资讯网——每日最新资讯28at.com

本文链接:http://www.28at.com/showinfo-26-38513-0.html理解 Wasm 基础概念,了解 Wasm 是如何被加载运行的?

声明:本网页内容旨在传播知识,不代表本站观点,若有侵权等问题请及时与本网联系,我们将在第一时间删除处理。

显示全文

上一篇:CSS_Flex 那些鲜为人知的内幕

下一篇:优化技巧:如何加快Spring项目启动速度

最新热点