wasm_exec文件解读

wasm_exec.js 文件解读

date: 23.10.7 go version: 1.21.1

因为我是笨蛋 所以有错误请让我知道

wasm_exec.js是go官方给出的 js解释Gom编译成的 wasm 的解释代码文件

当然也有其他的wasm_exec.js比如tinygo 本文旨在帮助不太熟悉前端的人( 可能只有我 )简单的去理解它的js这边是调用运行的 至于go那边则会放在后一篇

文件位于GOROOT下的misc/wasm文件夹下

$(go env GOROOT)/misc/wasm/wasm_exec.js

0~90 行

wasm相当于一个虚拟机它可以实现包括虚拟文件系统在内的系统功能 而这前90行的内容则是 判断wasm模块是否提供了系统环境 如果没有 让则直接返回enosys(error: not implemented) 表示系统功能未实现

globalThis: globalThis是JavaScript的一个全局对象,它是全局对象window的别名。在WebAssembly(WASM)环境中,由于没有全局对象window,因此需要使用globalThis来访问全局对象 在WASM模块中,我们可以使用globalThis来访问全局对象,如访问全局变量、全局函数等。例如,在模拟的文件系统API中,我们可以使用globalThis.fs来访问模拟的文件系统对象 需要注意的是,globalThis在浏览器环境中与window对象等效,但在其他环境中,如Node.js或Web Workers中,globalThis可能与global对象等效。因此,在编写跨不同环境运行的代码时,最好使用globalThis而不是window或global对象

特殊的: 它提供了 write(异步写)和writeSync(同步写)

writeSync(fd, buf) {
	outputBuf += decoder.decode(buf);
	const nl = outputBuf.lastIndexOf("\n");
	if (nl != -1) {
		console.log(outputBuf.substring(0, nl));
		outputBuf = outputBuf.substring(nl + 1);
	}
	return buf.length;
},
write(fd, buf, offset, length, position, callback) {
	if (offset !== 0 || length !== buf.length || position !== null) {
		callback(enosys());
		return;
	}
	const n = this.writeSync(fd, buf);
	callback(null, n);
},

Go.constructor(一) 95 ~ 209 行

js并不原生支持go的int64等类型 这一段主要实现了那些类型 包括( set* /get* /load* )

这些类型的实现使用的是小端法(little Endian) 即低位在前(低地址) 高位在后(高地址) 与常识的习惯相反请注意

小端法的优势: 四则运算时, 从低位每次取出相应字节运算, 最后直到高位, 最终把符号位刷新, 这样的运算方式会更高效

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
// exit(0) 正常退出
this.exit = (code) => {
    if (code !== 0) {
        console.warn("exit code:", code);
    }
};
this._exitPromise = new Promise((resolve) => {
    this._resolveExitPromise = resolve;
});
this._pendingEvent = null;
this._scheduledTimeouts = new Map();
this._nextCallbackTimeoutID = 1;

this._exitPromise: 这是一个Promise对象,用于表示WebSocket对象的关闭状态。当WebSocket对象被关闭时,它会被 resolved,从而触发相应的回调函数。

this._resolveExitPromise: 这是一个函数,用于处理_exitPromiseresolve操作。它将_exitPromise对象的resolve操作转发给_resolveExitPromise函数,以便在WebSocket对象被关闭时能够正确处理回调函数。

this._pendingEvent: 这是一个null值,表示WebSocket对象当前 pending 的事件。当WebSocket对象接收到新的事件时,它会更新_pendingEvent的值。

this._scheduledTimeouts: 这是一个Map对象,用于存储已经安排的回调函数。它存储回调函数的ID和回调函数本身,以便在指定的时间间隔内执行它们。

this._nextCallbackTimeoutID: 这是一个整数,表示下一个可用的回调函数ID。当需要为一个新的回调函数分配ID时,它会递增_nextCallbackTimeoutID的值。

1
2
3
4
5
6
const getInt64 = (addr) => {
    // DataView.getInt32(byteOffset: number, littleEndian?: boolean | undefined): number
    const low = this.mem.getUint32(addr + 0, true);
    const high = this.mem.getInt32(addr + 4, true);
    return low + high * 4294967296; // 2^32 即high<<32
}

this.mem 指代内存 在wasm环境中, 内存是一个虚拟的内存空间, 用于存储程序的变量和数据

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
// 将一个值( number/object/string/symbol/function )存储到内存中
const storeValue = (addr, v) => {
    // 0x7f800000是最大的float数 表示正无穷大
    const nanHead = 0x7FF80000;

    if (typeof v === "number" && v !== 0) {
        if (isNaN(v)) {
            this.mem.setUint32(addr + 4, nanHead, true);
            this.mem.setUint32(addr, 0, true);
            return;
        }
        this.mem.setFloat64(addr, v, true);
        return;
    }

    if (v === undefined) {
        this.mem.setFloat64(addr, 0, true);
        return;
    }

    let id = this._ids.get(v);
    if (id === undefined) {
        id = this._idPool.pop();
        if (id === undefined) {
            id = this._values.length;
        }
        this._values[id] = v;
        this._goRefCounts[id] = 0;
        this._ids.set(v, id);
    }
    this._goRefCounts[id]++;
    let typeFlag = 0;
    switch (typeof v) {
        case "object":
            if (v !== null) {
                typeFlag = 1;
            }
            break;
        case "string":
            typeFlag = 2;
            break;
        case "symbol":
            typeFlag = 3;
            break;
        case "function":
            typeFlag = 4;
            break;
    }
    this.mem.setUint32(addr + 4, nanHead | typeFlag, true);
    this.mem.setUint32(addr, id, true);
}

Go.constructor(二) 210 ~ 462 行

在golang提供的wasm_exec.html中 使用go.importObject作为WebAssembly.Imports来导入模块和函数

1
2
3
4
5
6
7
8
9
WebAssembly.instantiateStreaming(
    fetch("test.wasm"),
    go.importObject).then((result) => {
        mod = result.module;
        inst = result.instance;
        document.getElementById("runButton").disabled = false;
    }).catch((err) => {
        console.error(err);
});

210 ~ 462 行所描述的就是这个importObject

编译的wasm里头部那段import就是这里编成的(大概)

_gotest: 是测试类

gojs: 实现gojs包(和runtime包)里的接口的方法

以wasmExit为例

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
"runtime.wasmExit": (sp) => {
    sp >>>= 0;
    const code = this.mem.getInt32(sp + 8, true);
    this.exited = true;
    delete this._inst;
    delete this._values;
    delete this._goRefCounts;
    delete this._ids;
    delete this._idPool;
    this.exit(code);
},

直接调用 runtime.wasmExit(0) 的命令是 go.importObject.gojs["runtime.wasmExit"](0)

它实现的是 $(GOPATH)\src\runtime\sys_wasm.go中的接口

1
2
//go:wasmimport gojs runtime.wasmExit
func wasmExit(code int32)

go:wasmimport

在go1.21.1中引入一个新的指令 go:wasmimport

详见 https://go.dev/blog/wasi

todo:

尝试在go中直接调用runtime.wasmExit(0)失败了 提示找不到实现 无法成功编译

Go.run 465~540行

==以下两篇来自CodeGeeX==

1
async run(instance){...}

它是一个异步函数,用于与WebAssembly程序进行交互。这个函数的主要目的是将WebAssembly程序的内存映射到JavaScript内存,以便在WebAssembly和JavaScript之间进行数据交换

执行步骤如下:

  1. 函数首先检查传递给它的实例是否是WebAssembly.Instance类型,如果不是,则抛出一个错误。
  2. WebAssembly.Instance类型的实例存储在_inst变量中。
  3. 创建一个DataView对象mem,用于访问WebAssembly程序的内存。
  4. 创建一个包含一些JavaScript值(如NaN0nulltruefalseglobalThisthis)的数组_values。这个数组将存储WebAssembly程序当前正在引用的JS值。
  5. 创建一个数组_goRefCounts,用于存储WebAssembly程序对每个JS值的引用次数。数组的所有元素都被初始化为Infinity,表示它们被WebAssembly程序引用无穷多次。
  6. 创建一个Map对象_ids,用于将JS值映射到唯一的引用ID。
  7. 创建一个空数组_idPool,用于存储已被垃圾回收的引用ID。
  8. 设置exited变量为false,表示WebAssembly程序尚未退出。
  9. 创建一个Promise对象_exitPromise,用于在WebAssembly程序退出时通知JavaScript程序。
  10. 将命令行参数和环境变量写入WebAssembly程序的内存。这通过创建一个DataView对象argv,然后将命令行参数和环境变量添加到该对象中来完成。
  11. 调用WebAssembly程序的run函数,传递argcargv参数。argc表示命令行参数的数量,argv是一个指针数组,其中每个指针都指向一个命令行参数或环境变量。
  12. 如果exitedtrue,则调用_resolveExitPromise函数,以通知JavaScript程序WebAssembly程序已退出。
  13. 等待_exitPromise解析,然后继续执行。

_resume()和 _makeFuncWrapper(id)

_resume() 函数用于恢复Go程序的执行。它首先检查程序是否已经退出,如果是,则抛出一个错误。然后,它调用Go程序的resume()函数,这将执行Go程序中的代码。最后,它检查程序是否已经退出,如果是,则调用_resolveExitPromise()函数,该函数将通知JavaScript程序Go程序已退出。

_makeFuncWrapper(id) 函数用于创建一个函数,该函数可以调用Go程序中的函数,并将结果返回给JavaScript程序。它接受一个id参数,表示要调用的Go程序函数的ID。函数内部首先创建一个匿名函数,该函数将接收一个事件对象作为参数,该事件对象包含要调用的Go程序函数的ID、当前上下文和参数。然后,它将事件对象存储在_pendingEvent变量中,并调用_resume()函数来恢复Go程序的执行。最后,它返回事件对象中的结果。

updatedupdated2023-10-192023-10-19