@threadedstack/wmd-web 0.0.0-stage → 0.1.12
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2601 -2
- package/RELEASE.json +123 -0
- package/export-page/coi-sw.js +20 -0
- package/export-page/index.html +17 -0
- package/export-page/run.css +10 -0
- package/export-page/run.js +236 -0
- package/guest/arm64/Image +4 -0
- package/guest/arm64/Image-4k +4 -0
- package/guest/arm64/initramfs.cpio.gz +0 -0
- package/guest/arm64/vm.wasm +0 -0
- package/package.json +36 -4
- package/page/sw.js +381 -0
- package/relay/control.ts +139 -0
- package/relay/engine.ts +787 -0
- package/relay/hosted.ts +660 -0
- package/relay/keys.ts +53 -0
- package/relay/local-tls.ts +121 -0
- package/relay/lock.ts +81 -0
- package/relay/logfile.ts +138 -0
- package/relay/main.ts +642 -0
- package/relay/policy.ts +241 -0
- package/relay/relay.ts +266 -0
- package/relay/token.ts +116 -0
- package/relay/v1.ts +107 -0
- package/relay/wisp.ts +615 -0
- package/src/base.js +325 -0
- package/src/boot.js +2037 -0
- package/src/calib-worker.js +40 -0
- package/src/fs/9p/codec.js +303 -0
- package/src/fs/9p/server.js +521 -0
- package/src/fs/errno.js +43 -0
- package/src/fs/graph.js +380 -0
- package/src/fs/overlay.js +1018 -0
- package/src/fs/pack-write.js +165 -0
- package/src/fs/pack.js +427 -0
- package/src/fs/seed.js +22 -0
- package/src/fs/svc-snapshot.js +65 -0
- package/src/fs/upper-pack.js +112 -0
- package/src/host/base-cache.js +195 -0
- package/src/host/base-file.js +328 -0
- package/src/host/base-io.js +93 -0
- package/src/host/boot-events.js +209 -0
- package/src/host/chan-service.js +300 -0
- package/src/host/chan-services.js +689 -0
- package/src/host/chan.js +655 -0
- package/src/host/ckpt-file.js +395 -0
- package/src/host/ckpt-io.js +207 -0
- package/src/host/ckpt-point.js +160 -0
- package/src/host/ckpt-save.js +64 -0
- package/src/host/ckpt.js +434 -0
- package/src/host/clock.js +111 -0
- package/src/host/ctl.js +778 -0
- package/src/host/errno.js +18 -0
- package/src/host/fs.js +167 -0
- package/src/host/gcnudge.js +83 -0
- package/src/host/i64shim.js +81 -0
- package/src/host/jit-cache.js +341 -0
- package/src/host/jit-hub-core.js +208 -0
- package/src/host/jit-hub.js +93 -0
- package/src/host/jit-snap.js +226 -0
- package/src/host/jit.js +1903 -0
- package/src/host/layout.js +100 -0
- package/src/host/monitor.js +110 -0
- package/src/host/p9mount.js +27 -0
- package/src/host/ram-mem.js +125 -0
- package/src/host/stdio.js +143 -0
- package/src/host/threads.js +109 -0
- package/src/host/wasi.js +335 -0
- package/src/host/wmd-imports.js +71 -0
- package/src/host/yamux.js +387 -0
- package/src/image-files.js +154 -0
- package/src/net/dhcp-dns.js +126 -0
- package/src/net/net-worker.js +172 -0
- package/src/net/packet.js +227 -0
- package/src/net/peer.js +230 -0
- package/src/net/proto.js +189 -0
- package/src/net/relay-client.js +226 -0
- package/src/net/stack.js +867 -0
- package/src/net/switch.js +160 -0
- package/src/net/wisp-client.js +570 -0
- package/src/net/wisp-transport.js +196 -0
- package/src/net/wisp.js +511 -0
- package/src/registry/client.js +176 -0
- package/src/registry/converter.js +68 -0
- package/src/registry/hub.js +221 -0
- package/src/registry/oci.js +76 -0
- package/src/registry/pack-worker.js +70 -0
- package/src/registry/pull.js +258 -0
- package/src/registry/ref.js +110 -0
- package/src/registry/wasi-mem.js +355 -0
- package/src/worker.js +315 -0
- package/wmdpack.wasm +0 -0
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// W71: the WASI preview1 host wmdpack.wasm (cmd/wmdpack-wasm, union.PackLayer for wasip1) runs on in a
|
|
2
|
+
// Worker: one thread, an in-memory filesystem behind one preopen ("/"), stdin from bytes. It implements
|
|
3
|
+
// exactly the imports Go's wasip1 port links; anything else answers ENOSYS.
|
|
4
|
+
//
|
|
5
|
+
// const fs = new MemFs({ maxFileBytes })
|
|
6
|
+
// fs.addFile('below/0.pack', bytes) // read-only inputs
|
|
7
|
+
// const r = runWasi(module, { args: ['wmdpack', '/out.pack', '/below/0.pack'], stdin: layerBytes, fs })
|
|
8
|
+
// r.code, r.stdout, r.stderr; fs.take('out.pack') // the files the program wrote
|
|
9
|
+
//
|
|
10
|
+
// Paths are relative to the preopen ("/out.pack" in argv is "out.pack" here). Directories exist when a file
|
|
11
|
+
// path under them does (no empty directories are needed by the converter).
|
|
12
|
+
|
|
13
|
+
const E = Object.freeze({ SUCCESS: 0, BADF: 8, EXIST: 20, FBIG: 22, INVAL: 28, ISDIR: 31, NOENT: 44, NOSYS: 52, NOTDIR: 54, SPIPE: 70 })
|
|
14
|
+
const FT = Object.freeze({ CHAR: 2, DIR: 3, FILE: 4 })
|
|
15
|
+
const OFLAG = Object.freeze({ CREAT: 1, DIRECTORY: 2, EXCL: 4, TRUNC: 8 })
|
|
16
|
+
const FDFLAG_APPEND = 1
|
|
17
|
+
const PREOPEN = 3
|
|
18
|
+
|
|
19
|
+
class ExitSignal extends Error {
|
|
20
|
+
constructor(code) {
|
|
21
|
+
super(`exit ${code}`)
|
|
22
|
+
this.code = code
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Normalizes a path under the preopen; null when it climbs out. */
|
|
27
|
+
export function cleanPath(p) {
|
|
28
|
+
const out = []
|
|
29
|
+
for (const part of p.split('/')) {
|
|
30
|
+
if (part === '' || part === '.') continue
|
|
31
|
+
if (part === '..') {
|
|
32
|
+
if (!out.length) return null
|
|
33
|
+
out.pop()
|
|
34
|
+
continue
|
|
35
|
+
}
|
|
36
|
+
out.push(part)
|
|
37
|
+
}
|
|
38
|
+
return out.join('/')
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A file: its bytes (data[0..size)) and whether the program may change it. */
|
|
42
|
+
class MemFile {
|
|
43
|
+
constructor(data, size, readonly) {
|
|
44
|
+
this.data = data
|
|
45
|
+
this.size = size
|
|
46
|
+
this.readonly = readonly
|
|
47
|
+
}
|
|
48
|
+
ensure(n, max) {
|
|
49
|
+
if (n > max) return false
|
|
50
|
+
if (n <= this.data.length) return true
|
|
51
|
+
const grown = new Uint8Array(Math.min(max, Math.max(n, Math.ceil(this.data.length * 1.5), 1 << 20)))
|
|
52
|
+
grown.set(this.data.subarray(0, this.size))
|
|
53
|
+
this.data = grown
|
|
54
|
+
return true
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export class MemFs {
|
|
59
|
+
/** @param {{maxFileBytes?: number}} [o] a write past maxFileBytes fails with EFBIG */
|
|
60
|
+
constructor({ maxFileBytes = 4 * 2 ** 30 } = {}) {
|
|
61
|
+
this.files = new Map()
|
|
62
|
+
this.maxFileBytes = maxFileBytes
|
|
63
|
+
}
|
|
64
|
+
/** A read-only input file (the bytes are not copied). */
|
|
65
|
+
addFile(path, bytes) {
|
|
66
|
+
this.files.set(cleanPath(path), new MemFile(bytes, bytes.length, true))
|
|
67
|
+
}
|
|
68
|
+
/** The bytes of a file the program wrote, exactly its size; the file is removed from the fs. */
|
|
69
|
+
take(path) {
|
|
70
|
+
const p = cleanPath(path)
|
|
71
|
+
const f = this.files.get(p)
|
|
72
|
+
if (!f) return null
|
|
73
|
+
this.files.delete(p)
|
|
74
|
+
if (f.size === f.data.length) return f.data
|
|
75
|
+
const b = f.data.buffer
|
|
76
|
+
// ArrayBuffer.prototype.transfer (2024 engines) shrinks without a second copy
|
|
77
|
+
if (typeof b.transfer === 'function' && f.data.byteOffset === 0 && !(typeof SharedArrayBuffer !== 'undefined' && b instanceof SharedArrayBuffer)) {
|
|
78
|
+
return new Uint8Array(b.transfer(f.size))
|
|
79
|
+
}
|
|
80
|
+
return f.data.slice(0, f.size)
|
|
81
|
+
}
|
|
82
|
+
isDir(p) {
|
|
83
|
+
if (p === '') return true
|
|
84
|
+
const pre = `${p}/`
|
|
85
|
+
for (const k of this.files.keys()) if (k.startsWith(pre)) return true
|
|
86
|
+
return false
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Runs a wasip1 command to its end.
|
|
92
|
+
* @param {WebAssembly.Module} module
|
|
93
|
+
* @param {{args: string[], env?: string[], stdin?: Uint8Array, fs: MemFs}} o
|
|
94
|
+
* @returns {{code: number, stdout: string, stderr: string}}
|
|
95
|
+
*/
|
|
96
|
+
export function runWasi(module, { args, env = [], stdin = new Uint8Array(0), fs }) {
|
|
97
|
+
const enc = new TextEncoder()
|
|
98
|
+
const argv = args.map(a => enc.encode(`${a}\0`))
|
|
99
|
+
const envv = env.map(a => enc.encode(`${a}\0`))
|
|
100
|
+
const out = { 1: [], 2: [] }
|
|
101
|
+
let stdinPos = 0
|
|
102
|
+
// fd -> {kind: 'file', path, file, pos, append} | {kind: 'dir', path}
|
|
103
|
+
const fds = new Map([[PREOPEN, { kind: 'dir', path: '' }]])
|
|
104
|
+
let nextFd = 4
|
|
105
|
+
let memory = null
|
|
106
|
+
const u8 = () => new Uint8Array(memory.buffer)
|
|
107
|
+
const dv = () => new DataView(memory.buffer)
|
|
108
|
+
const str = (ptr, len) => new TextDecoder().decode(u8().subarray(ptr, ptr + len))
|
|
109
|
+
const resolve = (dirfd, ptr, len) => {
|
|
110
|
+
const d = fds.get(dirfd)
|
|
111
|
+
if (!d || d.kind !== 'dir') return { err: E.BADF }
|
|
112
|
+
const p = cleanPath(`${d.path}/${str(ptr, len)}`)
|
|
113
|
+
return p === null ? { err: E.INVAL } : { p }
|
|
114
|
+
}
|
|
115
|
+
const iovs = (ptr, n) => {
|
|
116
|
+
const v = dv()
|
|
117
|
+
const list = []
|
|
118
|
+
for (let i = 0; i < n; i++) list.push([v.getUint32(ptr + i * 8, true), v.getUint32(ptr + i * 8 + 4, true)])
|
|
119
|
+
return list
|
|
120
|
+
}
|
|
121
|
+
const putStrings = (list, ptrs, buf) => {
|
|
122
|
+
const v = dv()
|
|
123
|
+
const m = u8()
|
|
124
|
+
let o = buf
|
|
125
|
+
list.forEach((s, i) => {
|
|
126
|
+
v.setUint32(ptrs + i * 4, o, true)
|
|
127
|
+
m.set(s, o)
|
|
128
|
+
o += s.length
|
|
129
|
+
})
|
|
130
|
+
return E.SUCCESS
|
|
131
|
+
}
|
|
132
|
+
const sizes = (list, countPtr, sizePtr) => {
|
|
133
|
+
dv().setUint32(countPtr, list.length, true)
|
|
134
|
+
dv().setUint32(sizePtr, list.reduce((n, s) => n + s.length, 0), true)
|
|
135
|
+
return E.SUCCESS
|
|
136
|
+
}
|
|
137
|
+
const filestat = (ptr, type, size) => {
|
|
138
|
+
const v = dv()
|
|
139
|
+
for (let i = 0; i < 64; i += 8) v.setBigUint64(ptr + i, 0n, true)
|
|
140
|
+
v.setUint8(ptr + 16, type)
|
|
141
|
+
v.setBigUint64(ptr + 24, 1n, true)
|
|
142
|
+
v.setBigUint64(ptr + 32, BigInt(size), true)
|
|
143
|
+
return E.SUCCESS
|
|
144
|
+
}
|
|
145
|
+
const readInto = (src, srcSize, pos, list) => {
|
|
146
|
+
const m = u8()
|
|
147
|
+
let n = 0
|
|
148
|
+
for (const [p, len] of list) {
|
|
149
|
+
const k = Math.max(0, Math.min(len, srcSize - pos - n))
|
|
150
|
+
if (k > 0) m.set(src.subarray(pos + n, pos + n + k), p)
|
|
151
|
+
n += k
|
|
152
|
+
if (k < len) break
|
|
153
|
+
}
|
|
154
|
+
return n
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const wasi = {
|
|
158
|
+
args_sizes_get: (c, s) => sizes(argv, c, s),
|
|
159
|
+
args_get: (p, b) => putStrings(argv, p, b),
|
|
160
|
+
environ_sizes_get: (c, s) => sizes(envv, c, s),
|
|
161
|
+
environ_get: (p, b) => putStrings(envv, p, b),
|
|
162
|
+
clock_time_get: (_id, _prec, ptr) => {
|
|
163
|
+
dv().setBigUint64(ptr, BigInt(Math.round((performance.timeOrigin + performance.now()) * 1e6)), true)
|
|
164
|
+
return E.SUCCESS
|
|
165
|
+
},
|
|
166
|
+
random_get: (ptr, len) => {
|
|
167
|
+
for (let o = 0; o < len; o += 65536) crypto.getRandomValues(u8().subarray(ptr + o, ptr + Math.min(len, o + 65536)))
|
|
168
|
+
return E.SUCCESS
|
|
169
|
+
},
|
|
170
|
+
sched_yield: () => E.SUCCESS,
|
|
171
|
+
// Go's scheduler polls with clock timeouts when it is idle: report every subscription as due
|
|
172
|
+
// (one thread, nothing to wait for).
|
|
173
|
+
poll_oneoff: (inPtr, outPtr, n, neventsPtr) => {
|
|
174
|
+
const v = dv()
|
|
175
|
+
for (let i = 0; i < n; i++) {
|
|
176
|
+
const s = inPtr + i * 48
|
|
177
|
+
const e = outPtr + i * 32
|
|
178
|
+
v.setBigUint64(e, v.getBigUint64(s, true), true) // userdata
|
|
179
|
+
v.setUint16(e + 8, E.SUCCESS, true)
|
|
180
|
+
v.setUint8(e + 10, v.getUint8(s + 8)) // type
|
|
181
|
+
for (let k = 16; k < 32; k += 8) v.setBigUint64(e + k, 0n, true)
|
|
182
|
+
}
|
|
183
|
+
v.setUint32(neventsPtr, n, true)
|
|
184
|
+
return E.SUCCESS
|
|
185
|
+
},
|
|
186
|
+
proc_exit: code => {
|
|
187
|
+
throw new ExitSignal(code)
|
|
188
|
+
},
|
|
189
|
+
fd_write: (fd, iovPtr, n, nwritten) => {
|
|
190
|
+
const m = u8()
|
|
191
|
+
let total = 0
|
|
192
|
+
if (fd === 1 || fd === 2) {
|
|
193
|
+
for (const [p, len] of iovs(iovPtr, n)) {
|
|
194
|
+
out[fd].push(m.slice(p, p + len))
|
|
195
|
+
total += len
|
|
196
|
+
}
|
|
197
|
+
dv().setUint32(nwritten, total, true)
|
|
198
|
+
return E.SUCCESS
|
|
199
|
+
}
|
|
200
|
+
const f = fds.get(fd)
|
|
201
|
+
if (!f || f.kind !== 'file' || f.file.readonly) return E.BADF
|
|
202
|
+
const list = iovs(iovPtr, n)
|
|
203
|
+
const want = list.reduce((a, [, len]) => a + len, 0)
|
|
204
|
+
if (f.append) f.pos = f.file.size
|
|
205
|
+
if (!f.file.ensure(f.pos + want, fs.maxFileBytes)) return E.FBIG
|
|
206
|
+
const mm = u8()
|
|
207
|
+
for (const [p, len] of list) {
|
|
208
|
+
f.file.data.set(mm.subarray(p, p + len), f.pos)
|
|
209
|
+
f.pos += len
|
|
210
|
+
total += len
|
|
211
|
+
}
|
|
212
|
+
if (f.pos > f.file.size) f.file.size = f.pos
|
|
213
|
+
dv().setUint32(nwritten, total, true)
|
|
214
|
+
return E.SUCCESS
|
|
215
|
+
},
|
|
216
|
+
fd_read: (fd, iovPtr, n, nread) => {
|
|
217
|
+
if (fd === 0) {
|
|
218
|
+
const k = readInto(stdin, stdin.length, stdinPos, iovs(iovPtr, n))
|
|
219
|
+
stdinPos += k
|
|
220
|
+
dv().setUint32(nread, k, true)
|
|
221
|
+
return E.SUCCESS
|
|
222
|
+
}
|
|
223
|
+
const f = fds.get(fd)
|
|
224
|
+
if (!f) return E.BADF
|
|
225
|
+
if (f.kind !== 'file') return E.ISDIR
|
|
226
|
+
const k = readInto(f.file.data, f.file.size, f.pos, iovs(iovPtr, n))
|
|
227
|
+
f.pos += k
|
|
228
|
+
dv().setUint32(nread, k, true)
|
|
229
|
+
return E.SUCCESS
|
|
230
|
+
},
|
|
231
|
+
fd_pread: (fd, iovPtr, n, offset, nread) => {
|
|
232
|
+
const f = fds.get(fd)
|
|
233
|
+
if (!f) return E.BADF
|
|
234
|
+
if (f.kind !== 'file') return E.ISDIR
|
|
235
|
+
const k = readInto(f.file.data, f.file.size, Number(offset), iovs(iovPtr, n))
|
|
236
|
+
dv().setUint32(nread, k, true)
|
|
237
|
+
return E.SUCCESS
|
|
238
|
+
},
|
|
239
|
+
fd_seek: (fd, offset, whence, newPtr) => {
|
|
240
|
+
const f = fds.get(fd)
|
|
241
|
+
if (fd <= 2) return E.SPIPE
|
|
242
|
+
if (!f || f.kind !== 'file') return E.BADF
|
|
243
|
+
const base = whence === 0 ? 0 : whence === 1 ? f.pos : whence === 2 ? f.file.size : -1
|
|
244
|
+
if (base < 0) return E.INVAL
|
|
245
|
+
const pos = base + Number(offset)
|
|
246
|
+
if (pos < 0) return E.INVAL
|
|
247
|
+
f.pos = pos
|
|
248
|
+
dv().setBigUint64(newPtr, BigInt(pos), true)
|
|
249
|
+
return E.SUCCESS
|
|
250
|
+
},
|
|
251
|
+
fd_close: fd => {
|
|
252
|
+
if (fd <= PREOPEN) return E.SUCCESS
|
|
253
|
+
return fds.delete(fd) ? E.SUCCESS : E.BADF
|
|
254
|
+
},
|
|
255
|
+
fd_fdstat_get: (fd, ptr) => {
|
|
256
|
+
const v = dv()
|
|
257
|
+
const f = fds.get(fd)
|
|
258
|
+
const type = fd <= 2 ? FT.CHAR : f?.kind === 'dir' ? FT.DIR : f ? FT.FILE : -1
|
|
259
|
+
if (type < 0) return E.BADF
|
|
260
|
+
v.setUint8(ptr, type)
|
|
261
|
+
v.setUint16(ptr + 2, f?.append ? FDFLAG_APPEND : 0, true)
|
|
262
|
+
v.setBigUint64(ptr + 8, 0xffffffffffffffffn, true)
|
|
263
|
+
v.setBigUint64(ptr + 16, 0xffffffffffffffffn, true)
|
|
264
|
+
return E.SUCCESS
|
|
265
|
+
},
|
|
266
|
+
fd_fdstat_set_flags: () => E.SUCCESS,
|
|
267
|
+
fd_prestat_get: (fd, ptr) => {
|
|
268
|
+
if (fd !== PREOPEN) return E.BADF
|
|
269
|
+
dv().setUint32(ptr, 0, true) // directory
|
|
270
|
+
dv().setUint32(ptr + 4, 1, true) // "/"
|
|
271
|
+
return E.SUCCESS
|
|
272
|
+
},
|
|
273
|
+
fd_prestat_dir_name: (fd, ptr, len) => {
|
|
274
|
+
if (fd !== PREOPEN) return E.BADF
|
|
275
|
+
if (len >= 1) u8()[ptr] = 0x2f
|
|
276
|
+
return E.SUCCESS
|
|
277
|
+
},
|
|
278
|
+
fd_filestat_get: (fd, ptr) => {
|
|
279
|
+
if (fd <= 2) return filestat(ptr, FT.CHAR, 0)
|
|
280
|
+
const f = fds.get(fd)
|
|
281
|
+
if (!f) return E.BADF
|
|
282
|
+
return f.kind === 'dir' ? filestat(ptr, FT.DIR, 0) : filestat(ptr, FT.FILE, f.file.size)
|
|
283
|
+
},
|
|
284
|
+
path_filestat_get: (dirfd, _flags, ptr, len, out) => {
|
|
285
|
+
const r = resolve(dirfd, ptr, len)
|
|
286
|
+
if (r.err) return r.err
|
|
287
|
+
const f = fs.files.get(r.p)
|
|
288
|
+
if (f) return filestat(out, FT.FILE, f.size)
|
|
289
|
+
return fs.isDir(r.p) ? filestat(out, FT.DIR, 0) : E.NOENT
|
|
290
|
+
},
|
|
291
|
+
path_open: (dirfd, _dirflags, ptr, len, oflags, _rb, _ri, fdflags, fdOut) => {
|
|
292
|
+
const r = resolve(dirfd, ptr, len)
|
|
293
|
+
if (r.err) return r.err
|
|
294
|
+
let f = fs.files.get(r.p)
|
|
295
|
+
if (!f && fs.isDir(r.p)) {
|
|
296
|
+
if (oflags & OFLAG.CREAT) return E.ISDIR
|
|
297
|
+
const fd = nextFd++
|
|
298
|
+
fds.set(fd, { kind: 'dir', path: r.p })
|
|
299
|
+
dv().setUint32(fdOut, fd, true)
|
|
300
|
+
return E.SUCCESS
|
|
301
|
+
}
|
|
302
|
+
if (oflags & OFLAG.DIRECTORY) return f ? E.NOTDIR : E.NOENT
|
|
303
|
+
if (f && oflags & OFLAG.CREAT && oflags & OFLAG.EXCL) return E.EXIST
|
|
304
|
+
if (!f) {
|
|
305
|
+
if (!(oflags & OFLAG.CREAT)) return E.NOENT
|
|
306
|
+
const parent = r.p.includes('/') ? r.p.slice(0, r.p.lastIndexOf('/')) : ''
|
|
307
|
+
if (parent && fs.files.has(parent)) return E.NOTDIR
|
|
308
|
+
f = new MemFile(new Uint8Array(0), 0, false)
|
|
309
|
+
fs.files.set(r.p, f)
|
|
310
|
+
} else if (oflags & OFLAG.TRUNC) {
|
|
311
|
+
if (f.readonly) return E.BADF
|
|
312
|
+
f.size = 0
|
|
313
|
+
}
|
|
314
|
+
const fd = nextFd++
|
|
315
|
+
fds.set(fd, { kind: 'file', path: r.p, file: f, pos: 0, append: !!(fdflags & FDFLAG_APPEND) })
|
|
316
|
+
dv().setUint32(fdOut, fd, true)
|
|
317
|
+
return E.SUCCESS
|
|
318
|
+
},
|
|
319
|
+
path_rename: (fd1, p1, l1, fd2, p2, l2) => {
|
|
320
|
+
const a = resolve(fd1, p1, l1)
|
|
321
|
+
const b = resolve(fd2, p2, l2)
|
|
322
|
+
if (a.err || b.err) return a.err || b.err
|
|
323
|
+
const f = fs.files.get(a.p)
|
|
324
|
+
if (!f) return E.NOENT
|
|
325
|
+
fs.files.delete(a.p)
|
|
326
|
+
fs.files.set(b.p, f)
|
|
327
|
+
return E.SUCCESS
|
|
328
|
+
},
|
|
329
|
+
path_unlink_file: (dirfd, ptr, len) => {
|
|
330
|
+
const r = resolve(dirfd, ptr, len)
|
|
331
|
+
if (r.err) return r.err
|
|
332
|
+
if (!fs.files.has(r.p)) return fs.isDir(r.p) ? E.ISDIR : E.NOENT
|
|
333
|
+
fs.files.delete(r.p)
|
|
334
|
+
return E.SUCCESS
|
|
335
|
+
},
|
|
336
|
+
path_remove_directory: () => E.NOSYS,
|
|
337
|
+
path_readlink: () => E.INVAL,
|
|
338
|
+
fd_readdir: () => E.NOSYS,
|
|
339
|
+
}
|
|
340
|
+
|
|
341
|
+
const inst = new WebAssembly.Instance(module, {
|
|
342
|
+
wasi_snapshot_preview1: new Proxy(wasi, { get: (t, k) => t[k] || (() => E.NOSYS) }),
|
|
343
|
+
})
|
|
344
|
+
memory = inst.exports.memory
|
|
345
|
+
let code = 0
|
|
346
|
+
try {
|
|
347
|
+
inst.exports._start()
|
|
348
|
+
} catch (e) {
|
|
349
|
+
if (!(e instanceof ExitSignal)) throw e
|
|
350
|
+
code = e.code
|
|
351
|
+
}
|
|
352
|
+
const text = parts => new TextDecoder().decode(parts.length === 1 ? parts[0] : new Uint8Array(parts.flatMap(p => [...p])))
|
|
353
|
+
// memBytes: the module's linear memory when it ended (wasm memory only grows: its peak)
|
|
354
|
+
return { code, stdout: text(out[1]), stderr: text(out[2]), memBytes: memory.buffer.byteLength }
|
|
355
|
+
}
|
package/src/worker.js
ADDED
|
@@ -0,0 +1,315 @@
|
|
|
1
|
+
// One Worker = one wasm thread of vm.wasm (the runner's one-Store-per-thread model,
|
|
2
|
+
// runner/src/host.rs, spawn.rs).
|
|
3
|
+
//
|
|
4
|
+
// role "main": instantiate vm.wasm and call _start (tid 1, vCPU 0).
|
|
5
|
+
// role "pool": park on a pool slot; for each spawn request instantiate vm.wasm again
|
|
6
|
+
// (same shared memory, a fresh table) and call wasi_thread_start(tid, start_arg).
|
|
7
|
+
//
|
|
8
|
+
// Messages to the page: {t:'ready'|'exit'|'trap'|'need'|'thread'}; output goes through
|
|
9
|
+
// the shared stdio rings (host/stdio.js).
|
|
10
|
+
|
|
11
|
+
import { createClock } from './host/clock.js'
|
|
12
|
+
import { knownLookup } from './host/jit-cache.js'
|
|
13
|
+
import { createFs } from './host/fs.js'
|
|
14
|
+
import { gcNudge, gcNudger, jitYielder, needsGcNudge } from './host/gcnudge.js'
|
|
15
|
+
import { shimI64Imports } from './host/i64shim.js'
|
|
16
|
+
import {
|
|
17
|
+
CTL_EXIT,
|
|
18
|
+
CTL_EXIT_CODE,
|
|
19
|
+
CTL_CK_RESTORED,
|
|
20
|
+
CTL_POOL_GEN,
|
|
21
|
+
CTL_STALL_DONE,
|
|
22
|
+
CTL_STALL_MS,
|
|
23
|
+
CTL_STALL_TAKEN,
|
|
24
|
+
CTL_STALL_TID,
|
|
25
|
+
CTL_STALL_WAKE,
|
|
26
|
+
CTL_TRAPS,
|
|
27
|
+
F64_STALL_AT,
|
|
28
|
+
F64_STALL_END,
|
|
29
|
+
SLOT_STATE,
|
|
30
|
+
ST_IDLE,
|
|
31
|
+
slotIndex,
|
|
32
|
+
} from './host/layout.js'
|
|
33
|
+
import { createThreadSpawn, threadDone, waitForWork } from './host/threads.js'
|
|
34
|
+
import { WasiExit, createWasi } from './host/wasi.js'
|
|
35
|
+
import { createJitDoor, poolProbe, TABLE_MIN } from './host/jit.js'
|
|
36
|
+
import { stdioWriter } from './host/stdio.js'
|
|
37
|
+
import { createWmdImports } from './host/wmd-imports.js'
|
|
38
|
+
import { createRamImports } from './host/ram-mem.js'
|
|
39
|
+
import { createChanImports } from './host/chan.js'
|
|
40
|
+
import { createCkptImports } from './host/ckpt-point.js'
|
|
41
|
+
|
|
42
|
+
const enc = new TextEncoder()
|
|
43
|
+
|
|
44
|
+
self.onmessage = e => {
|
|
45
|
+
const m = e.data
|
|
46
|
+
if (m.t !== 'init') return
|
|
47
|
+
run(m).catch(err => fatal(m, err, 0))
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// JSPI: lets a vCPU's jit_sync wait (its wasm stack suspended, the Worker's event loop
|
|
51
|
+
// running) for a Module another Worker compiled, instead of compiling its own copy.
|
|
52
|
+
// W32: how often a WebKit JIT thread yields to its event loop at a wmd.jit_yield point
|
|
53
|
+
export const YIELD_MS = 2000
|
|
54
|
+
export const haveJspi = () => typeof WebAssembly.Suspending === 'function' && typeof WebAssembly.promising === 'function'
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* W77: the JIT door's side of the page's module cache (host/jit-cache.js): the index table
|
|
58
|
+
* the page read before the guest started, module reads and notes over the cache port.
|
|
59
|
+
*/
|
|
60
|
+
function jitCacheClient({ sab, port }) {
|
|
61
|
+
const waiting = new Map()
|
|
62
|
+
let next = 1
|
|
63
|
+
port.onmessage = e => {
|
|
64
|
+
const r = waiting.get(e.data.id)
|
|
65
|
+
if (!r) return
|
|
66
|
+
waiting.delete(e.data.id)
|
|
67
|
+
r(e.data.bytes || null)
|
|
68
|
+
}
|
|
69
|
+
return {
|
|
70
|
+
lookup: knownLookup(sab),
|
|
71
|
+
request(hl, hh, len) {
|
|
72
|
+
const id = next++
|
|
73
|
+
return new Promise(resolve => {
|
|
74
|
+
waiting.set(id, resolve)
|
|
75
|
+
port.postMessage({ t: 'get', id, hl, hh, len })
|
|
76
|
+
})
|
|
77
|
+
},
|
|
78
|
+
note(msg) {
|
|
79
|
+
port.postMessage({ t: 'note', ...msg })
|
|
80
|
+
},
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function fatal(m, err, tid) {
|
|
85
|
+
if (err instanceof WasiExit) return
|
|
86
|
+
const ctl = new Int32Array(m.ctlSab)
|
|
87
|
+
Atomics.add(ctl, CTL_TRAPS, 1) // counted even when another thread's exit won CTL_EXIT
|
|
88
|
+
// A trap on any thread ends the machine, as in the runner (exit status 4).
|
|
89
|
+
if (Atomics.compareExchange(ctl, CTL_EXIT, 0, 1) === 0) {
|
|
90
|
+
Atomics.store(ctl, CTL_EXIT_CODE, 4)
|
|
91
|
+
Atomics.notify(ctl, CTL_EXIT)
|
|
92
|
+
}
|
|
93
|
+
self.postMessage({
|
|
94
|
+
t: 'trap',
|
|
95
|
+
role: m.role,
|
|
96
|
+
slot: m.slot,
|
|
97
|
+
tid,
|
|
98
|
+
message: String(err && err.message ? err.message : err),
|
|
99
|
+
stack: err && err.stack ? String(err.stack) : '',
|
|
100
|
+
})
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
async function run(m) {
|
|
104
|
+
const { module, memory, ctlSab, role, slot } = m
|
|
105
|
+
const ctl = new Int32Array(ctlSab)
|
|
106
|
+
const clock = createClock(ctlSab)
|
|
107
|
+
const fs = createFs({ ctlSab, preopen: m.preopen, files: m.files })
|
|
108
|
+
|
|
109
|
+
// stdout/stderr go into the shared rings (host/stdio.js); the page drains them.
|
|
110
|
+
const write = stdioWriter(m.stdio)
|
|
111
|
+
const wasi = createWasi({
|
|
112
|
+
memory,
|
|
113
|
+
args: m.args,
|
|
114
|
+
env: m.env,
|
|
115
|
+
fs,
|
|
116
|
+
clock,
|
|
117
|
+
ctl,
|
|
118
|
+
write,
|
|
119
|
+
onExit: code => self.postMessage({ t: 'exit', code, role, slot }),
|
|
120
|
+
})
|
|
121
|
+
const threadSpawn = createThreadSpawn(ctl, {
|
|
122
|
+
requestWorkers: why => self.postMessage({ t: 'need', why }),
|
|
123
|
+
waitMs: m.spawnWaitMs ?? 10000,
|
|
124
|
+
})
|
|
125
|
+
// The JIT door (P-B2): one per Worker over the machine's shared slot list. With a
|
|
126
|
+
// hub port (sharing, P-B2 adversarial F1) the imports that sync may return a Promise:
|
|
127
|
+
// they are wrapped in WebAssembly.Suspending and the guest entry points in
|
|
128
|
+
// WebAssembly.promising, so the wasm stack suspends while the Module arrives.
|
|
129
|
+
// Only a module that imports the door gets one: a synthetic guest (the P-B1 review's
|
|
130
|
+
// test/e2e/review-synth/*.wat) has no wmd.* imports and no vm_helper_* exports.
|
|
131
|
+
// vm.wasm's table import minimum, read from its bytes on the page (tableMinOf); the JIT
|
|
132
|
+
// slots start right after it.
|
|
133
|
+
const tableMin = m.tableMin ?? TABLE_MIN
|
|
134
|
+
const wantsJit = !!m.jit && WebAssembly.Module.imports(module).some(i => i.module === 'wmd' && i.name === 'jit_load')
|
|
135
|
+
const sharing = wantsJit && !!m.jitPort
|
|
136
|
+
if (sharing && !haveJspi()) throw new Error('JIT module sharing needs JSPI (WebAssembly.Suspending) in this Worker')
|
|
137
|
+
const jit = wantsJit
|
|
138
|
+
? createJitDoor({
|
|
139
|
+
store: m.jit,
|
|
140
|
+
memory,
|
|
141
|
+
ram: m.ram || null,
|
|
142
|
+
log: s => write(2, enc.encode(`${s}\n`)),
|
|
143
|
+
compileOnCompile: m.jitOpts?.compileOnCompile,
|
|
144
|
+
brk: m.jitOpts?.brk,
|
|
145
|
+
maxLiveBytes: m.jitOpts?.maxLiveBytes,
|
|
146
|
+
codePool: m.jitOpts?.codePool !== false && needsGcNudge() ? poolProbe : null,
|
|
147
|
+
share: sharing ? { port: m.jitPort, waitMs: m.jitOpts?.shareWaitMs } : null,
|
|
148
|
+
tableMin,
|
|
149
|
+
cache: m.jitCache ? jitCacheClient(m.jitCache) : null,
|
|
150
|
+
})
|
|
151
|
+
: null
|
|
152
|
+
let jitImports = jit?.imports
|
|
153
|
+
if (sharing) {
|
|
154
|
+
const S = f => new WebAssembly.Suspending(f)
|
|
155
|
+
jitImports = { ...jitImports, load: S(jitImports.load), sync: S(jitImports.sync), flush: S(jitImports.flush) }
|
|
156
|
+
// W77, D-513: a hit on a module this Worker has not got yet waits for it (the page's bytes, the hub's Module)
|
|
157
|
+
jitImports.known = S(jitImports.known)
|
|
158
|
+
// W32: WebKit gives released JIT code back only from a thread's event loop; vm.wasm calls
|
|
159
|
+
// wmd.jit_yield where no JIT block is on its stack (host/gcnudge.js jitYielder)
|
|
160
|
+
if (needsGcNudge()) {
|
|
161
|
+
const ms = m.jitOpts?.yieldMs ?? 0
|
|
162
|
+
jitImports.yield = S(jitYielder({ every: m.jitOpts?.yieldEvery ?? YIELD_MS, delay: fn => setTimeout(fn, ms) }))
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const enter = f => (sharing ? WebAssembly.promising(f) : f)
|
|
166
|
+
// P-B3: channels over the chan block's rings, when the boot has channel services.
|
|
167
|
+
const chan = m.chan ? createChanImports({ sab: m.chan, memory, exiting: () => Atomics.load(ctl, CTL_EXIT) !== 0 }) : {}
|
|
168
|
+
let heapTopFn = null // vm.wasm's vm_heap_top export, once the main thread has instantiated (null: a full scan)
|
|
169
|
+
// P-B9: checkpoint support: wmd.ctl_attach and wmd.point (host/ckpt-point.js)
|
|
170
|
+
const ckpt = m.ckpt ? createCkptImports({ memory, ctl, scanLimit: m.ckptScanLimit ?? null, verifyBound: !!m.ckptVerifyBound, useHeapTop: !!m.ckptUseHeapTop, heapTop: () => heapTopFn?.() ?? null, post: (msg, transfer) => self.postMessage(msg, transfer || []) }) : null
|
|
171
|
+
// the flat layout: guest RAM is the page's memory64 (host/ram-mem.js)
|
|
172
|
+
const ramImports = m.ram ? createRamImports({ memory, ram: m.ram }) : null
|
|
173
|
+
const wmd = createWmdImports({
|
|
174
|
+
jit: jitImports,
|
|
175
|
+
chan,
|
|
176
|
+
ckpt,
|
|
177
|
+
ram: ramImports,
|
|
178
|
+
counters: ctl, // stub calls (chan_open, snapshot, jit_* without a door) are counted in ctl, read by the page
|
|
179
|
+
})
|
|
180
|
+
|
|
181
|
+
// Once the machine has ended (a trap on any thread, or proc_exit), every other thread
|
|
182
|
+
// stops at its next host call instead of running on until the page terminates its
|
|
183
|
+
// Worker: after a trap the guest's memory may hold garbage (P-B11 review finding 4: a
|
|
184
|
+
// second vCPU went on to interpret freed IR and printed "vreg out of range"). The JIT
|
|
185
|
+
// door's imports are left as they are.
|
|
186
|
+
const stopped = () => Atomics.load(ctl, CTL_EXIT) !== 0
|
|
187
|
+
// P-B8 test hook (bootWmd({stallHook: true}), vm.stallThread): the thread the page names
|
|
188
|
+
// blocks at its next host call for the time asked, as if the browser had stopped its
|
|
189
|
+
// Worker (a GC pause, an OS deschedule). A vCPU reads the guest's clock through
|
|
190
|
+
// clock_time_get many times a second, so it is reached within milliseconds; the guest
|
|
191
|
+
// state it stops in (a lock held, an interrupt pending) is wherever that call was.
|
|
192
|
+
let myTid = role === 'main' ? 1 : 0
|
|
193
|
+
const f64 = new Float64Array(ctlSab)
|
|
194
|
+
const maybeStall = () => {
|
|
195
|
+
if (Atomics.load(ctl, CTL_STALL_TID) !== myTid || !myTid) return
|
|
196
|
+
if (Atomics.compareExchange(ctl, CTL_STALL_TID, myTid, 0) !== myTid) return
|
|
197
|
+
const ms = Atomics.load(ctl, CTL_STALL_MS)
|
|
198
|
+
const wake = Atomics.load(ctl, CTL_STALL_WAKE)
|
|
199
|
+
f64[F64_STALL_AT / 8] = performance.timeOrigin + performance.now()
|
|
200
|
+
Atomics.add(ctl, CTL_STALL_TAKEN, 1)
|
|
201
|
+
Atomics.wait(ctl, CTL_STALL_WAKE, wake, ms)
|
|
202
|
+
f64[F64_STALL_END / 8] = performance.timeOrigin + performance.now()
|
|
203
|
+
Atomics.add(ctl, CTL_STALL_DONE, 1)
|
|
204
|
+
}
|
|
205
|
+
const stallHook = !!m.stallHook
|
|
206
|
+
// D-419: WebKit collects this thread's garbage only when nudged (host/gcnudge.js)
|
|
207
|
+
const nudge = gcNudger()
|
|
208
|
+
// W32: an idle pool Worker yields to its event loop (below). The vm.wasm threads' own yield
|
|
209
|
+
// points are not here: a JSPI suspension from clock_time_get trapped in WebKit ("JSPI stack scan
|
|
210
|
+
// failed: unrecognized type of Wasm frame: RestoreFrameMode") when a JIT block chain was on the
|
|
211
|
+
// stack (blocks tail-call across instances), so vm.wasm must choose where it yields.
|
|
212
|
+
const idleYield = sharing && !!nudge
|
|
213
|
+
const guard = (fns, skip = () => false) =>
|
|
214
|
+
Object.fromEntries(
|
|
215
|
+
Object.entries(fns).map(([k, f]) =>
|
|
216
|
+
typeof f !== 'function' || (skip(k) && !nudge)
|
|
217
|
+
? [k, f]
|
|
218
|
+
: skip(k)
|
|
219
|
+
? [k, (a, b, c, d, e, g, h, i, j) => (nudge(), f(a, b, c, d, e, g, h, i, j))] // the JIT door's compile thread allocates most
|
|
220
|
+
: // fixed arity, not (...a): an argument array per call is garbage, which WebKit
|
|
221
|
+
// collects late in a thread holding the guest's memory (W40, D-419). Nine covers
|
|
222
|
+
// every import (path_open has nine parameters).
|
|
223
|
+
[k, (a, b, c, d, e, g, h, i, j) => {
|
|
224
|
+
nudge?.()
|
|
225
|
+
if (stallHook) maybeStall()
|
|
226
|
+
if (stopped()) throw new WasiExit(Atomics.load(ctl, CTL_EXIT_CODE))
|
|
227
|
+
return f(a, b, c, d, e, g, h, i, j)
|
|
228
|
+
}],
|
|
229
|
+
),
|
|
230
|
+
)
|
|
231
|
+
const wasiG = guard(wasi, k => k === 'proc_exit')
|
|
232
|
+
const wmdG = guard(wmd, k => k.startsWith('jit_'))
|
|
233
|
+
const spawnG = guard({ 'thread-spawn': threadSpawn })
|
|
234
|
+
|
|
235
|
+
function instantiate() {
|
|
236
|
+
const table = new WebAssembly.Table({ element: 'anyfunc', initial: tableMin })
|
|
237
|
+
// D-419: the imports with i64 parameters go through a wasm shim: JS gets Numbers, no BigInts
|
|
238
|
+
const instance = new WebAssembly.Instance(module, shimI64Imports({
|
|
239
|
+
env: { memory, __indirect_function_table: table },
|
|
240
|
+
// ram_rw and ram_cas stay wasm functions (no guard, no BigInt): a page-table walk calls them
|
|
241
|
+
wmd: ramImports ? { ...wmdG, ram_rw: ramImports.ram_rw, ram_cas: ramImports.ram_cas } : wmdG,
|
|
242
|
+
wasi_snapshot_preview1: wasiG,
|
|
243
|
+
wasi: spawnG,
|
|
244
|
+
}))
|
|
245
|
+
if (table.length !== tableMin) throw new Error(`table size ${table.length} after instantiation, want ${tableMin}`)
|
|
246
|
+
jit?.attach(instance, table)
|
|
247
|
+
return instance
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
if (role === 'main') {
|
|
251
|
+
const inst = instantiate()
|
|
252
|
+
if (typeof inst.exports.vm_heap_top === 'function') heapTopFn = () => Number(inst.exports.vm_heap_top())
|
|
253
|
+
self.postMessage({ t: 'ready', role, at: performance.timeOrigin + performance.now() })
|
|
254
|
+
try {
|
|
255
|
+
if (m.resume) {
|
|
256
|
+
// P-B9: a restored machine. The page wrote the checkpoint's memory; this is vm.wasm's
|
|
257
|
+
// resume entry (guest/vm/src/main_wasm.c vm_resume), the way the runner's resume() does it:
|
|
258
|
+
// bring this thread's table up to date, write the new host ports where the guest reads
|
|
259
|
+
// them, and call vm_resume instead of _start.
|
|
260
|
+
// The instance is made on a memory the checkpoint has not touched yet, as the runner's resume() does
|
|
261
|
+
// (instantiate, then restore the pages): vm.wasm's start function (__wasm_init_memory) initialises the
|
|
262
|
+
// static data AND this thread's TLS base (__tls_base) only for the first instance on a memory; on a
|
|
263
|
+
// memory that already holds the checkpoint (its init flag set) it skips both and the main thread would
|
|
264
|
+
// run with __tls_base 0. The page writes the pages now and wakes us.
|
|
265
|
+
self.postMessage({ t: 'resume-instantiated' })
|
|
266
|
+
const r = Atomics.wait(ctl, CTL_CK_RESTORED, 0, 120_000)
|
|
267
|
+
if (r === 'timed-out') throw new Error('the page did not restore the checkpoint memory in 120 s')
|
|
268
|
+
if (Atomics.load(ctl, CTL_CK_RESTORED) === 2) throw new Error('the page could not restore the checkpoint memory (see its message)')
|
|
269
|
+
await jit?.syncAsync()
|
|
270
|
+
const at = (await enter(inst.exports.vm_resume_args)()) >>> 0
|
|
271
|
+
const ports = m.resume.ports // net, p9, console, control (0: none), then stats
|
|
272
|
+
new Int32Array(memory.buffer, at, 5).set([ports.net | 0, ports.p9 | 0, ports.console | 0, ports.control | 0, m.resume.stats ? 1 : 0])
|
|
273
|
+
const code = await enter(inst.exports.vm_resume)()
|
|
274
|
+
wasi.proc_exit(code)
|
|
275
|
+
return
|
|
276
|
+
}
|
|
277
|
+
await enter(inst.exports._start)()
|
|
278
|
+
// wasi-libc's _start always ends in proc_exit; returning means exit(0).
|
|
279
|
+
wasi.proc_exit(0)
|
|
280
|
+
} catch (err) {
|
|
281
|
+
fatal(m, err, 1)
|
|
282
|
+
}
|
|
283
|
+
return
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Pool Worker: announce, then loop serving spawn requests.
|
|
287
|
+
Atomics.store(ctl, slotIndex(slot, SLOT_STATE), ST_IDLE)
|
|
288
|
+
Atomics.add(ctl, CTL_POOL_GEN, 1)
|
|
289
|
+
Atomics.notify(ctl, CTL_POOL_GEN)
|
|
290
|
+
self.postMessage({ t: 'ready', role, slot })
|
|
291
|
+
for (;;) {
|
|
292
|
+
let work = waitForWork(ctl, slot, 1000, idleYield ? 5 : Infinity)
|
|
293
|
+
// W32: an idle pool Worker yields to its event loop every 5 s, so WebKit sweeps the
|
|
294
|
+
// Instance of the thread it ran last (see idleYield above)
|
|
295
|
+
while (work === undefined) {
|
|
296
|
+
gcNudge()
|
|
297
|
+
await new Promise(r => setTimeout(r, 0))
|
|
298
|
+
work = waitForWork(ctl, slot, 1000, 5)
|
|
299
|
+
}
|
|
300
|
+
if (!work) return
|
|
301
|
+
self.postMessage({ t: 'thread', ev: 'start', tid: work.tid, slot })
|
|
302
|
+
myTid = work.tid
|
|
303
|
+
let inst
|
|
304
|
+
try {
|
|
305
|
+
inst = instantiate()
|
|
306
|
+
await jit?.syncAsync() // a new thread's table starts with every live slot (spawn.rs)
|
|
307
|
+
await enter(inst.exports.wasi_thread_start)(work.tid, work.arg)
|
|
308
|
+
} catch (err) {
|
|
309
|
+
fatal(m, err, work.tid)
|
|
310
|
+
return
|
|
311
|
+
}
|
|
312
|
+
threadDone(ctl, slot)
|
|
313
|
+
self.postMessage({ t: 'thread', ev: 'end', tid: work.tid, slot })
|
|
314
|
+
}
|
|
315
|
+
}
|
package/wmdpack.wasm
ADDED
|
Binary file
|