@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.
Files changed (92) hide show
  1. package/README.md +2601 -2
  2. package/RELEASE.json +123 -0
  3. package/export-page/coi-sw.js +20 -0
  4. package/export-page/index.html +17 -0
  5. package/export-page/run.css +10 -0
  6. package/export-page/run.js +236 -0
  7. package/guest/arm64/Image +4 -0
  8. package/guest/arm64/Image-4k +4 -0
  9. package/guest/arm64/initramfs.cpio.gz +0 -0
  10. package/guest/arm64/vm.wasm +0 -0
  11. package/package.json +36 -4
  12. package/page/sw.js +381 -0
  13. package/relay/control.ts +139 -0
  14. package/relay/engine.ts +787 -0
  15. package/relay/hosted.ts +660 -0
  16. package/relay/keys.ts +53 -0
  17. package/relay/local-tls.ts +121 -0
  18. package/relay/lock.ts +81 -0
  19. package/relay/logfile.ts +138 -0
  20. package/relay/main.ts +642 -0
  21. package/relay/policy.ts +241 -0
  22. package/relay/relay.ts +266 -0
  23. package/relay/token.ts +116 -0
  24. package/relay/v1.ts +107 -0
  25. package/relay/wisp.ts +615 -0
  26. package/src/base.js +325 -0
  27. package/src/boot.js +2037 -0
  28. package/src/calib-worker.js +40 -0
  29. package/src/fs/9p/codec.js +303 -0
  30. package/src/fs/9p/server.js +521 -0
  31. package/src/fs/errno.js +43 -0
  32. package/src/fs/graph.js +380 -0
  33. package/src/fs/overlay.js +1018 -0
  34. package/src/fs/pack-write.js +165 -0
  35. package/src/fs/pack.js +427 -0
  36. package/src/fs/seed.js +22 -0
  37. package/src/fs/svc-snapshot.js +65 -0
  38. package/src/fs/upper-pack.js +112 -0
  39. package/src/host/base-cache.js +195 -0
  40. package/src/host/base-file.js +328 -0
  41. package/src/host/base-io.js +93 -0
  42. package/src/host/boot-events.js +209 -0
  43. package/src/host/chan-service.js +300 -0
  44. package/src/host/chan-services.js +689 -0
  45. package/src/host/chan.js +655 -0
  46. package/src/host/ckpt-file.js +395 -0
  47. package/src/host/ckpt-io.js +207 -0
  48. package/src/host/ckpt-point.js +160 -0
  49. package/src/host/ckpt-save.js +64 -0
  50. package/src/host/ckpt.js +434 -0
  51. package/src/host/clock.js +111 -0
  52. package/src/host/ctl.js +778 -0
  53. package/src/host/errno.js +18 -0
  54. package/src/host/fs.js +167 -0
  55. package/src/host/gcnudge.js +83 -0
  56. package/src/host/i64shim.js +81 -0
  57. package/src/host/jit-cache.js +341 -0
  58. package/src/host/jit-hub-core.js +208 -0
  59. package/src/host/jit-hub.js +93 -0
  60. package/src/host/jit-snap.js +226 -0
  61. package/src/host/jit.js +1903 -0
  62. package/src/host/layout.js +100 -0
  63. package/src/host/monitor.js +110 -0
  64. package/src/host/p9mount.js +27 -0
  65. package/src/host/ram-mem.js +125 -0
  66. package/src/host/stdio.js +143 -0
  67. package/src/host/threads.js +109 -0
  68. package/src/host/wasi.js +335 -0
  69. package/src/host/wmd-imports.js +71 -0
  70. package/src/host/yamux.js +387 -0
  71. package/src/image-files.js +154 -0
  72. package/src/net/dhcp-dns.js +126 -0
  73. package/src/net/net-worker.js +172 -0
  74. package/src/net/packet.js +227 -0
  75. package/src/net/peer.js +230 -0
  76. package/src/net/proto.js +189 -0
  77. package/src/net/relay-client.js +226 -0
  78. package/src/net/stack.js +867 -0
  79. package/src/net/switch.js +160 -0
  80. package/src/net/wisp-client.js +570 -0
  81. package/src/net/wisp-transport.js +196 -0
  82. package/src/net/wisp.js +511 -0
  83. package/src/registry/client.js +176 -0
  84. package/src/registry/converter.js +68 -0
  85. package/src/registry/hub.js +221 -0
  86. package/src/registry/oci.js +76 -0
  87. package/src/registry/pack-worker.js +70 -0
  88. package/src/registry/pull.js +258 -0
  89. package/src/registry/ref.js +110 -0
  90. package/src/registry/wasi-mem.js +355 -0
  91. package/src/worker.js +315 -0
  92. 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