@kupcad/wasm 0.0.0-stage → 0.1.1

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/src/wasi.js ADDED
@@ -0,0 +1,127 @@
1
+ // WASI (preview 1) for kupcad.wasm: what its C and Zig runtimes ask of a host, answered
2
+ // without one, the same in a browser and in Node. The files a script reads come with its call,
3
+ // in the module's own memory (`files`), so it gets no file system: no directory is preopened
4
+ // and every file call answers ENOSYS. Its clocks and randomness are the host's, and what it writes
5
+ // to stdout and stderr, its log, goes to a callback a line at a time.
6
+
7
+ const ERRNO = { SUCCESS: 0, BADF: 8, INVAL: 28, NOSYS: 52 };
8
+ const CLOCK = { REALTIME: 0, MONOTONIC: 1, PROCESS_CPUTIME: 2, THREAD_CPUTIME: 3 };
9
+ const FILETYPE_CHARACTER_DEVICE = 2;
10
+ const STDOUT = 1;
11
+ const STDERR = 2;
12
+ /** The clocks' resolution, in nanoseconds: a microsecond. */
13
+ const CLOCK_RESOLUTION_NS = 1000n;
14
+ const NS_PER_MS = 1_000_000;
15
+ /** The most `crypto.getRandomValues` fills at once. */
16
+ const RANDOM_CHUNK_BYTES = 65536;
17
+ /** `__wasi_fdstat_t`: a u8 file type, u16 flags, two u64 rights. */
18
+ const FDSTAT_BYTES = 24;
19
+ /** `__wasi_ciovec_t`: a u32 pointer and a u32 length. */
20
+ const IOVEC_BYTES = 8;
21
+
22
+ /** The module called `proc_exit`: it stopped, and its instance can't be used again. */
23
+ export class WasiExit extends Error {
24
+ /** @param {number} code */
25
+ constructor(code) {
26
+ super(`kupcad.wasm exited with code ${code}`);
27
+ this.name = 'WasiExit';
28
+ this.code = code;
29
+ }
30
+ }
31
+
32
+ /**
33
+ * The WASI imports of a module, and the memory they work in, which the instance gives once it
34
+ * exists (`bind`).
35
+ * @param {(line: string) => void} log where the module's log goes, a line at a time
36
+ */
37
+ export function createWasi(log) {
38
+ /** @type {WebAssembly.Memory | null} */
39
+ let memory = null;
40
+ /** @type {Map<number, { decoder: TextDecoder, line: string }>} */
41
+ const streams = new Map([
42
+ [STDOUT, { decoder: new TextDecoder(), line: '' }],
43
+ [STDERR, { decoder: new TextDecoder(), line: '' }],
44
+ ]);
45
+ const view = () => new DataView(/** @type {WebAssembly.Memory} */ (memory).buffer);
46
+ const bytes = () => new Uint8Array(/** @type {WebAssembly.Memory} */ (memory).buffer);
47
+
48
+ /** @type {Record<string, (...args: any[]) => number>} */
49
+ const calls = {
50
+ /** @param {number} id @param {number} out */
51
+ clock_res_get(id, out) {
52
+ if (!Object.values(CLOCK).includes(id)) return ERRNO.INVAL;
53
+ view().setBigUint64(out, CLOCK_RESOLUTION_NS, true);
54
+ return ERRNO.SUCCESS;
55
+ },
56
+ /** @param {number} id @param {bigint} _precision @param {number} out */
57
+ clock_time_get(id, _precision, out) {
58
+ let ns;
59
+ if (id === CLOCK.REALTIME) ns = BigInt(Date.now()) * BigInt(NS_PER_MS);
60
+ else if (Object.values(CLOCK).includes(id))
61
+ ns = BigInt(Math.round(performance.now() * NS_PER_MS));
62
+ else return ERRNO.INVAL;
63
+ view().setBigUint64(out, ns, true);
64
+ return ERRNO.SUCCESS;
65
+ },
66
+ /** @param {number} buf @param {number} len */
67
+ random_get(buf, len) {
68
+ for (let at = 0; at < len; at += RANDOM_CHUNK_BYTES) {
69
+ crypto.getRandomValues(
70
+ bytes().subarray(buf + at, buf + Math.min(len, at + RANDOM_CHUNK_BYTES)),
71
+ );
72
+ }
73
+ return ERRNO.SUCCESS;
74
+ },
75
+ /** @param {number} fd @param {number} iovs @param {number} count @param {number} nwritten */
76
+ fd_write(fd, iovs, count, nwritten) {
77
+ const stream = streams.get(fd);
78
+ if (!stream) return ERRNO.BADF;
79
+ let written = 0;
80
+ for (let i = 0; i < count; i++) {
81
+ const ptr = view().getUint32(iovs + i * IOVEC_BYTES, true);
82
+ const len = view().getUint32(iovs + i * IOVEC_BYTES + 4, true);
83
+ stream.line += stream.decoder.decode(bytes().subarray(ptr, ptr + len), { stream: true });
84
+ written += len;
85
+ }
86
+ const lines = stream.line.split('\n');
87
+ stream.line = /** @type {string} */ (lines.pop());
88
+ for (const line of lines) log(line);
89
+ view().setUint32(nwritten, written, true);
90
+ return ERRNO.SUCCESS;
91
+ },
92
+ /** @param {number} fd @param {number} out */
93
+ fd_fdstat_get(fd, out) {
94
+ if (fd > STDERR) return ERRNO.BADF;
95
+ bytes().fill(0, out, out + FDSTAT_BYTES);
96
+ view().setUint8(out, FILETYPE_CHARACTER_DEVICE);
97
+ return ERRNO.SUCCESS;
98
+ },
99
+ // No directory is preopened: wasi-libc asks from fd 3 on, until this
100
+ fd_prestat_get: () => ERRNO.BADF,
101
+ /** @param {number} code */
102
+ proc_exit(code) {
103
+ throw new WasiExit(code);
104
+ },
105
+ };
106
+
107
+ return {
108
+ /**
109
+ * The `wasi_snapshot_preview1` functions `module` imports: each one this answers, the rest
110
+ * ENOSYS.
111
+ * @param {WebAssembly.Module} module
112
+ * @returns {Record<string, (...args: any[]) => number>}
113
+ */
114
+ imports(module) {
115
+ /** @type {Record<string, (...args: any[]) => number>} */
116
+ const out = {};
117
+ for (const { module: from, name } of WebAssembly.Module.imports(module)) {
118
+ if (from === 'wasi_snapshot_preview1') out[name] = calls[name] ?? (() => ERRNO.NOSYS);
119
+ }
120
+ return out;
121
+ },
122
+ /** @param {WebAssembly.Memory} instanceMemory the module's memory, once it is instantiated */
123
+ bind(instanceMemory) {
124
+ memory = instanceMemory;
125
+ },
126
+ };
127
+ }
package/src/worker.js ADDED
@@ -0,0 +1,8 @@
1
+ // The worker's entry (createKupcadWorker starts it): the core serves the page on the worker's
2
+ // own port.
3
+
4
+ import { serve } from './serve.js';
5
+
6
+ /** @import { PortLike } from './types.js' */
7
+
8
+ serve(/** @type {PortLike} */ (/** @type {unknown} */ (globalThis)));