lilac-wasm-bin 0.2.0
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.
- checksums.yaml +7 -0
- data/README.md +46 -0
- data/data/lilac-compiled.wasm +0 -0
- data/data/lilac-full.wasm +0 -0
- data/data/mrbc-host.wasm +0 -0
- data/data/mruby-wasm-js/_memory.js +35 -0
- data/data/mruby-wasm-js/_smoke.ts +117 -0
- data/data/mruby-wasm-js/debug.js +8 -0
- data/data/mruby-wasm-js/index.d.ts +177 -0
- data/data/mruby-wasm-js/index.js +461 -0
- data/data/mruby-wasm-js/wasi-preview1.js +705 -0
- data/lib/lilac/wasm/bin/version.rb +9 -0
- data/lib/lilac/wasm/bin.rb +101 -0
- metadata +67 -0
|
@@ -0,0 +1,705 @@
|
|
|
1
|
+
// WASI preview1 implementation (in-memory) for mruby-wasm-js.
|
|
2
|
+
//
|
|
3
|
+
// `createWasiPreview1({ env, args, stdin, fs })` returns a fresh,
|
|
4
|
+
// independent WASI implementation with its own state — env vars, argv,
|
|
5
|
+
// stdin buffer, virtual filesystem, open-fd table. Used as the default
|
|
6
|
+
// `wasi_snapshot_preview1` import object by index.js's `createVM`,
|
|
7
|
+
// but also exportable for direct use.
|
|
8
|
+
//
|
|
9
|
+
// Internal layout:
|
|
10
|
+
// - File / Directory: tree-VFS node classes (module-level)
|
|
11
|
+
// - constants: WASI errno / oflags / fdflags / filetype values
|
|
12
|
+
// - pure tree walkers: pathSegments / lookupFull / lookupNode /
|
|
13
|
+
// ensureParent / walkFiles / resolveRelative — pass `root` as arg
|
|
14
|
+
// - createFsFacade(root): Map-compatible facade over a Directory tree
|
|
15
|
+
// - createWasiPreview1(options): orchestrator that holds per-VM state
|
|
16
|
+
// (open fds, stdout buffer, instance ref) and returns the WASI
|
|
17
|
+
// imports object plus VFS handles.
|
|
18
|
+
|
|
19
|
+
import { debug } from "./debug.js";
|
|
20
|
+
import { createMemoryHelpers, decoder, encoder } from "./_memory.js";
|
|
21
|
+
|
|
22
|
+
// --- Module-level types ---------------------------------------------------
|
|
23
|
+
|
|
24
|
+
/** A regular-file node in the VFS. Holds raw bytes; no path stored. */
|
|
25
|
+
export class File {
|
|
26
|
+
constructor(data = new Uint8Array(0)) {
|
|
27
|
+
this.data = data;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** A directory node in the VFS. `entries` maps name → File | Directory. */
|
|
32
|
+
export class Directory {
|
|
33
|
+
constructor(entries = {}) {
|
|
34
|
+
this.entries = entries;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// --- Constants ------------------------------------------------------------
|
|
39
|
+
|
|
40
|
+
// path_open oflags
|
|
41
|
+
const O_CREAT = 1;
|
|
42
|
+
const O_DIRECTORY = 2;
|
|
43
|
+
const O_EXCL = 4;
|
|
44
|
+
const O_TRUNC = 8;
|
|
45
|
+
// path_open / fd_fdstat fdflags
|
|
46
|
+
const FD_APPEND = 1;
|
|
47
|
+
// fd_seek whence
|
|
48
|
+
const WHENCE_SET = 0;
|
|
49
|
+
const WHENCE_CUR = 1;
|
|
50
|
+
const WHENCE_END = 2;
|
|
51
|
+
// preview1 errno values we actually return
|
|
52
|
+
const E_BADF = 8;
|
|
53
|
+
const E_EXIST = 20;
|
|
54
|
+
const E_INVAL = 28;
|
|
55
|
+
const E_ISDIR = 31;
|
|
56
|
+
const E_NOENT = 44;
|
|
57
|
+
const E_NOTDIR = 54;
|
|
58
|
+
const E_NOTEMPTY = 55;
|
|
59
|
+
// filetype values written into filestat records
|
|
60
|
+
const FILETYPE_CHARACTER_DEVICE = 2;
|
|
61
|
+
const FILETYPE_DIRECTORY = 3;
|
|
62
|
+
const FILETYPE_REGULAR_FILE = 4;
|
|
63
|
+
|
|
64
|
+
const PREOPEN_FD = 3;
|
|
65
|
+
const PREOPEN_PATH = "/";
|
|
66
|
+
|
|
67
|
+
// --- Pure tree walkers ----------------------------------------------------
|
|
68
|
+
// All take `root` (or another Directory) as an explicit argument so they
|
|
69
|
+
// can be unit-tested independently of WASI imports / VM state.
|
|
70
|
+
|
|
71
|
+
// Normalise an absolute path to an array of segments. Empty + "."
|
|
72
|
+
// segments are dropped, ".." pops the previous segment.
|
|
73
|
+
function pathSegments(absPath) {
|
|
74
|
+
const out = [];
|
|
75
|
+
for (const seg of absPath.split("/")) {
|
|
76
|
+
if (seg === "" || seg === ".") continue;
|
|
77
|
+
if (seg === "..") out.pop();
|
|
78
|
+
else out.push(seg);
|
|
79
|
+
}
|
|
80
|
+
return out;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function resolvePathToAbs(rel) {
|
|
84
|
+
return (PREOPEN_PATH + "/" + rel).replace(/\/+/g, "/");
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// Walk an absolute path. Returns one of:
|
|
88
|
+
// { parent, name, node } — node is the resolved File|Directory or null if missing
|
|
89
|
+
// { parent: null, name: "", node: root } — root itself
|
|
90
|
+
// null — traversal hit a File mid-path (caller maps to E_NOTDIR)
|
|
91
|
+
function lookupFull(root, absPath) {
|
|
92
|
+
const segs = pathSegments(absPath);
|
|
93
|
+
if (segs.length === 0) return { parent: null, name: "", node: root };
|
|
94
|
+
let dir = root;
|
|
95
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
96
|
+
const next = dir.entries[segs[i]];
|
|
97
|
+
if (next == null) return { parent: dir, name: segs[segs.length - 1], node: null };
|
|
98
|
+
if (!(next instanceof Directory)) return null;
|
|
99
|
+
dir = next;
|
|
100
|
+
}
|
|
101
|
+
const leaf = segs[segs.length - 1];
|
|
102
|
+
return { parent: dir, name: leaf, node: dir.entries[leaf] ?? null };
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function lookupNode(root, absPath) {
|
|
106
|
+
const r = lookupFull(root, absPath);
|
|
107
|
+
return r ? r.node : null;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Walk to (or create) the parent Directory of absPath. Auto-creates
|
|
111
|
+
// intermediate Directory nodes; throws if any intermediate is a File.
|
|
112
|
+
function ensureParent(root, absPath) {
|
|
113
|
+
const segs = pathSegments(absPath);
|
|
114
|
+
if (segs.length === 0) throw new Error("cannot ensure parent of root");
|
|
115
|
+
let dir = root;
|
|
116
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
117
|
+
const name = segs[i];
|
|
118
|
+
let next = dir.entries[name];
|
|
119
|
+
if (next == null) {
|
|
120
|
+
next = new Directory();
|
|
121
|
+
dir.entries[name] = next;
|
|
122
|
+
} else if (!(next instanceof Directory)) {
|
|
123
|
+
throw new Error(`cannot create '${absPath}': '${name}' is a file`);
|
|
124
|
+
}
|
|
125
|
+
dir = next;
|
|
126
|
+
}
|
|
127
|
+
return { parent: dir, leaf: segs[segs.length - 1] };
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Walk all File leaves yielding [absolutePath, bytes] pairs.
|
|
131
|
+
function* walkFiles(prefix, dir) {
|
|
132
|
+
for (const [name, node] of Object.entries(dir.entries)) {
|
|
133
|
+
const path = prefix + "/" + name;
|
|
134
|
+
if (node instanceof File) yield [path, node.data];
|
|
135
|
+
else yield* walkFiles(path, node);
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
// Walk relPath from baseDir; "." skipped, ".." treated as "stay put"
|
|
140
|
+
// (we don't track parent pointers — good enough for readdir's fstatat).
|
|
141
|
+
function resolveRelative(baseDir, relPath) {
|
|
142
|
+
const segs = relPath.split("/").filter((s) => s.length > 0 && s !== ".");
|
|
143
|
+
let node = baseDir;
|
|
144
|
+
for (const seg of segs) {
|
|
145
|
+
if (seg === "..") continue;
|
|
146
|
+
if (!(node instanceof Directory)) return null;
|
|
147
|
+
node = node.entries[seg] ?? null;
|
|
148
|
+
if (!node) return null;
|
|
149
|
+
}
|
|
150
|
+
return node;
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
// --- Pure record writers --------------------------------------------------
|
|
154
|
+
|
|
155
|
+
// Write a 64-byte WASI filestat record. Only fields we care about
|
|
156
|
+
// (filetype, nlink, size) are filled; timestamps and dev/ino stay 0.
|
|
157
|
+
function writeFilestat(view, ptr, filetype, size) {
|
|
158
|
+
for (let i = 0; i < 64; i++) view.setUint8(ptr + i, 0);
|
|
159
|
+
view.setUint8(ptr + 16, filetype);
|
|
160
|
+
view.setBigUint64(ptr + 24, 1n, true);
|
|
161
|
+
view.setBigUint64(ptr + 32, BigInt(size), true);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Sum the byte length across an iovec array (uint32 length lives at
|
|
165
|
+
// offset 4 of each 8-byte slot — ptr at offset 0, len at offset 4).
|
|
166
|
+
function iovsTotalLen(view, iovsPtr, iovsLen) {
|
|
167
|
+
let total = 0;
|
|
168
|
+
for (let i = 0; i < iovsLen; i++) total += view.getUint32(iovsPtr + i * 8 + 4, true);
|
|
169
|
+
return total;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// --- Public: fs Map facade factory ----------------------------------------
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Build a Map-compatible facade over a `Directory` tree. Exposes
|
|
176
|
+
* set / get / has / delete / iteration / clear / size / Symbol.iterator
|
|
177
|
+
* plus `populate(dir)` and `root`. Iteration yields only File leaves
|
|
178
|
+
* in tree-traversal order. `set` auto-creates intermediate Directory
|
|
179
|
+
* nodes on demand.
|
|
180
|
+
*/
|
|
181
|
+
export function createFsFacade(root) {
|
|
182
|
+
return {
|
|
183
|
+
set(path, bytes) {
|
|
184
|
+
const { parent, leaf } = ensureParent(root, path);
|
|
185
|
+
const existing = parent.entries[leaf];
|
|
186
|
+
if (existing instanceof Directory) {
|
|
187
|
+
throw new Error(`cannot set '${path}': it's a directory`);
|
|
188
|
+
}
|
|
189
|
+
if (existing instanceof File) existing.data = bytes;
|
|
190
|
+
else parent.entries[leaf] = new File(bytes);
|
|
191
|
+
return this;
|
|
192
|
+
},
|
|
193
|
+
get(path) {
|
|
194
|
+
const node = lookupNode(root, path);
|
|
195
|
+
return node instanceof File ? node.data : undefined;
|
|
196
|
+
},
|
|
197
|
+
has(path) {
|
|
198
|
+
return lookupNode(root, path) instanceof File;
|
|
199
|
+
},
|
|
200
|
+
delete(path) {
|
|
201
|
+
const r = lookupFull(root, path);
|
|
202
|
+
if (!r || !(r.node instanceof File) || !r.parent) return false;
|
|
203
|
+
delete r.parent.entries[r.name];
|
|
204
|
+
return true;
|
|
205
|
+
},
|
|
206
|
+
*entries() { yield* walkFiles("", root); },
|
|
207
|
+
*keys() { for (const [p] of this.entries()) yield p; },
|
|
208
|
+
*values() { for (const [, v] of this.entries()) yield v; },
|
|
209
|
+
[Symbol.iterator]() { return this.entries(); },
|
|
210
|
+
get size() { let n = 0; for (const _ of this.entries()) n++; return n; },
|
|
211
|
+
clear() { root.entries = {}; },
|
|
212
|
+
populate(dir) {
|
|
213
|
+
if (!(dir instanceof Directory)) throw new TypeError("fs.populate expects a Directory");
|
|
214
|
+
root.entries = dir.entries;
|
|
215
|
+
},
|
|
216
|
+
get root() { return root; },
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// --- IO helpers (module-level, state-injected) ----------------------------
|
|
221
|
+
// Each helper takes an `io` state object containing exactly the mutable
|
|
222
|
+
// references it touches. Hoisting these out of the factory mirrors the
|
|
223
|
+
// "tree walker" treatment for read/write code paths and lets unit tests
|
|
224
|
+
// drive them with synthetic state ({ fs: mockFs, ... }) without
|
|
225
|
+
// instantiating wasm.
|
|
226
|
+
|
|
227
|
+
// fd_write helper: write iovec contents into the in-memory file backing
|
|
228
|
+
// `f`. Grows `io.fs` entry if needed; advances f.pos for non-append fds.
|
|
229
|
+
function writeToOpenFile({ fs }, f, view, memory, iovsPtr, iovsLen) {
|
|
230
|
+
const data = fs.get(f.path);
|
|
231
|
+
const needed = iovsTotalLen(view, iovsPtr, iovsLen);
|
|
232
|
+
const writeStart = f.append ? data.length : f.pos;
|
|
233
|
+
const newSize = Math.max(data.length, writeStart + needed);
|
|
234
|
+
let target = data;
|
|
235
|
+
if (newSize > data.length) {
|
|
236
|
+
target = new Uint8Array(newSize);
|
|
237
|
+
target.set(data);
|
|
238
|
+
fs.set(f.path, target);
|
|
239
|
+
}
|
|
240
|
+
let pos = writeStart;
|
|
241
|
+
let total = 0;
|
|
242
|
+
for (let i = 0; i < iovsLen; i++) {
|
|
243
|
+
const ptr = view.getUint32(iovsPtr + i * 8, true);
|
|
244
|
+
const len = view.getUint32(iovsPtr + i * 8 + 4, true);
|
|
245
|
+
target.set(memory.subarray(ptr, ptr + len), pos);
|
|
246
|
+
pos += len;
|
|
247
|
+
total += len;
|
|
248
|
+
}
|
|
249
|
+
if (!f.append) f.pos = pos;
|
|
250
|
+
return total;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// fd_read helper: drain bytes from the JS-side stdin buffer into iovec
|
|
254
|
+
// slots. Returns 0 (EOF) when buffer is empty.
|
|
255
|
+
function readFromStdin({ stdin }, view, memory, iovsPtr, iovsLen) {
|
|
256
|
+
let total = 0;
|
|
257
|
+
for (let i = 0; i < iovsLen; i++) {
|
|
258
|
+
const ptr = view.getUint32(iovsPtr + i * 8, true);
|
|
259
|
+
const len = view.getUint32(iovsPtr + i * 8 + 4, true);
|
|
260
|
+
const remaining = stdin.bytes.length;
|
|
261
|
+
if (remaining <= 0) break;
|
|
262
|
+
const n = Math.min(len, remaining);
|
|
263
|
+
memory.set(stdin.bytes.subarray(0, n), ptr);
|
|
264
|
+
stdin.bytes = stdin.bytes.subarray(n);
|
|
265
|
+
total += n;
|
|
266
|
+
}
|
|
267
|
+
return total;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// fd_read helper: drain bytes from an open file's backing array into
|
|
271
|
+
// iovec slots. Advances f.pos. Returns -1 if the path was removed under
|
|
272
|
+
// us (caller maps to E_BADF).
|
|
273
|
+
function readFromOpenFile({ fs }, f, view, memory, iovsPtr, iovsLen) {
|
|
274
|
+
const data = fs.get(f.path);
|
|
275
|
+
if (!data) return -1;
|
|
276
|
+
let total = 0;
|
|
277
|
+
for (let i = 0; i < iovsLen; i++) {
|
|
278
|
+
const ptr = view.getUint32(iovsPtr + i * 8, true);
|
|
279
|
+
const len = view.getUint32(iovsPtr + i * 8 + 4, true);
|
|
280
|
+
const remaining = data.length - f.pos;
|
|
281
|
+
if (remaining <= 0) break;
|
|
282
|
+
const n = Math.min(len, remaining);
|
|
283
|
+
memory.set(data.subarray(f.pos, f.pos + n), ptr);
|
|
284
|
+
f.pos += n;
|
|
285
|
+
total += n;
|
|
286
|
+
}
|
|
287
|
+
return total;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// fd_write helper: drain iovec contents into a line-buffered console.
|
|
291
|
+
// fd 1/2 (and any unknown fd that isn't a tracked file) lands here so
|
|
292
|
+
// `puts` from mruby reaches console.log without spawning a real tty.
|
|
293
|
+
function writeToStdio({ stdoutBuffer }, view, memory, iovsPtr, iovsLen) {
|
|
294
|
+
let total = 0;
|
|
295
|
+
for (let i = 0; i < iovsLen; i++) {
|
|
296
|
+
const ptr = view.getUint32(iovsPtr + i * 8, true);
|
|
297
|
+
const len = view.getUint32(iovsPtr + i * 8 + 4, true);
|
|
298
|
+
const str = decoder.decode(memory.subarray(ptr, ptr + len));
|
|
299
|
+
stdoutBuffer.push(str);
|
|
300
|
+
if (str.includes("\n")) {
|
|
301
|
+
const joined = stdoutBuffer.join("");
|
|
302
|
+
stdoutBuffer.length = 0;
|
|
303
|
+
for (const line of joined.split("\n")) {
|
|
304
|
+
if (line.length > 0) console.log("[mruby]", line);
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
total += len;
|
|
308
|
+
}
|
|
309
|
+
return total;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
// --- Stdin helper (module-level) ------------------------------------------
|
|
313
|
+
|
|
314
|
+
function makeStdin(initial) {
|
|
315
|
+
let bytes;
|
|
316
|
+
if (initial == null) bytes = new Uint8Array(0);
|
|
317
|
+
else if (typeof initial === "string") bytes = encoder.encode(initial);
|
|
318
|
+
else if (initial instanceof Uint8Array) bytes = initial;
|
|
319
|
+
else throw new TypeError("stdin must be a string, Uint8Array, or undefined");
|
|
320
|
+
return {
|
|
321
|
+
bytes,
|
|
322
|
+
pushText(text) {
|
|
323
|
+
const add = encoder.encode(text);
|
|
324
|
+
const merged = new Uint8Array(this.bytes.length + add.length);
|
|
325
|
+
merged.set(this.bytes);
|
|
326
|
+
merged.set(add, this.bytes.length);
|
|
327
|
+
this.bytes = merged;
|
|
328
|
+
},
|
|
329
|
+
};
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
// --- WASI factory ---------------------------------------------------------
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Build a fresh WASI preview1 implementation. Each call gets independent
|
|
336
|
+
* state (env, args, stdin, fs, open-fd table, stdout buffering).
|
|
337
|
+
*
|
|
338
|
+
* @returns {{
|
|
339
|
+
* imports: object,
|
|
340
|
+
* bindInstance: (inst: WebAssembly.Instance) => void,
|
|
341
|
+
* env: Record<string, string>,
|
|
342
|
+
* args: string[],
|
|
343
|
+
* stdin: { bytes: Uint8Array, pushText: (s: string) => void },
|
|
344
|
+
* fs: object,
|
|
345
|
+
* }}
|
|
346
|
+
*/
|
|
347
|
+
export function createWasiPreview1(options = {}) {
|
|
348
|
+
const env = { ...(options.env ?? {}) };
|
|
349
|
+
const args = [...(options.args ?? ["mruby-wasm-js"])];
|
|
350
|
+
const stdin = makeStdin(options.stdin);
|
|
351
|
+
const root = options.fs instanceof Directory ? options.fs : new Directory();
|
|
352
|
+
const fs = createFsFacade(root);
|
|
353
|
+
|
|
354
|
+
let nextFileFd = 4;
|
|
355
|
+
// fd → { type: 'file', path, pos, append } | { type: 'dir', node }
|
|
356
|
+
const openFiles = new Map();
|
|
357
|
+
const stdoutBuffer = [];
|
|
358
|
+
let instance = null;
|
|
359
|
+
|
|
360
|
+
const { readUtf8 } = createMemoryHelpers(() => instance);
|
|
361
|
+
|
|
362
|
+
// Single state bag passed to module-level IO helpers below. One
|
|
363
|
+
// allocation up-front instead of per-call object literals.
|
|
364
|
+
const io = { fs, stdin, stdoutBuffer };
|
|
365
|
+
|
|
366
|
+
// --- WASI imports ------------------------------------------------------
|
|
367
|
+
const imports = {
|
|
368
|
+
fd_write(fd, iovsPtr, iovsLen, nwrittenPtr) {
|
|
369
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
370
|
+
const memory = new Uint8Array(instance.exports.memory.buffer);
|
|
371
|
+
const f = openFiles.get(fd);
|
|
372
|
+
if (f && f.type === "dir") return E_ISDIR;
|
|
373
|
+
const total = f
|
|
374
|
+
? writeToOpenFile(io, f, view, memory, iovsPtr, iovsLen)
|
|
375
|
+
: writeToStdio(io, view, memory, iovsPtr, iovsLen);
|
|
376
|
+
view.setUint32(nwrittenPtr, total, true);
|
|
377
|
+
return 0;
|
|
378
|
+
},
|
|
379
|
+
fd_close(fd) {
|
|
380
|
+
if (openFiles.has(fd)) openFiles.delete(fd);
|
|
381
|
+
return 0;
|
|
382
|
+
},
|
|
383
|
+
fd_seek(fd, offset, whence, newOffsetPtr) {
|
|
384
|
+
const f = openFiles.get(fd);
|
|
385
|
+
if (!f || f.type !== "file") return E_BADF;
|
|
386
|
+
const data = fs.get(f.path);
|
|
387
|
+
if (!data) return E_BADF;
|
|
388
|
+
const off = Number(offset);
|
|
389
|
+
let newPos;
|
|
390
|
+
switch (whence) {
|
|
391
|
+
case WHENCE_SET: newPos = off; break;
|
|
392
|
+
case WHENCE_CUR: newPos = f.pos + off; break;
|
|
393
|
+
case WHENCE_END: newPos = data.length + off; break;
|
|
394
|
+
default: return E_INVAL;
|
|
395
|
+
}
|
|
396
|
+
if (newPos < 0) return E_INVAL;
|
|
397
|
+
f.pos = newPos;
|
|
398
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
399
|
+
view.setBigUint64(newOffsetPtr, BigInt(newPos), true);
|
|
400
|
+
return 0;
|
|
401
|
+
},
|
|
402
|
+
fd_tell(fd, ptr) {
|
|
403
|
+
const f = openFiles.get(fd);
|
|
404
|
+
if (!f || f.type !== "file") return E_BADF;
|
|
405
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
406
|
+
view.setBigUint64(ptr, BigInt(f.pos), true);
|
|
407
|
+
return 0;
|
|
408
|
+
},
|
|
409
|
+
fd_fdstat_get(_fd, fdstatPtr) {
|
|
410
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
411
|
+
for (let off = 0; off < 24; off++) view.setUint8(fdstatPtr + off, 0);
|
|
412
|
+
return 0;
|
|
413
|
+
},
|
|
414
|
+
fd_fdstat_set_flags(_fd, _flags) { return 0; },
|
|
415
|
+
fd_filestat_get(fd, ptr) {
|
|
416
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
417
|
+
const f = openFiles.get(fd);
|
|
418
|
+
if (f && f.type === "file") {
|
|
419
|
+
const data = fs.get(f.path);
|
|
420
|
+
if (!data) return E_BADF;
|
|
421
|
+
writeFilestat(view, ptr, FILETYPE_REGULAR_FILE, data.length);
|
|
422
|
+
return 0;
|
|
423
|
+
}
|
|
424
|
+
if (f && f.type === "dir") {
|
|
425
|
+
writeFilestat(view, ptr, FILETYPE_DIRECTORY, 0);
|
|
426
|
+
return 0;
|
|
427
|
+
}
|
|
428
|
+
if (fd === 0 || fd === 1 || fd === 2) {
|
|
429
|
+
writeFilestat(view, ptr, FILETYPE_CHARACTER_DEVICE, 0);
|
|
430
|
+
return 0;
|
|
431
|
+
}
|
|
432
|
+
return E_BADF;
|
|
433
|
+
},
|
|
434
|
+
fd_prestat_get(fd, ptr) {
|
|
435
|
+
if (fd !== PREOPEN_FD) return E_BADF;
|
|
436
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
437
|
+
const nameBytes = encoder.encode(PREOPEN_PATH);
|
|
438
|
+
view.setUint8(ptr, 0);
|
|
439
|
+
view.setUint32(ptr + 4, nameBytes.length, true);
|
|
440
|
+
return 0;
|
|
441
|
+
},
|
|
442
|
+
fd_prestat_dir_name(fd, ptr, len) {
|
|
443
|
+
if (fd !== PREOPEN_FD) return E_BADF;
|
|
444
|
+
const memory = new Uint8Array(instance.exports.memory.buffer);
|
|
445
|
+
const nameBytes = encoder.encode(PREOPEN_PATH);
|
|
446
|
+
const n = Math.min(nameBytes.length, len);
|
|
447
|
+
memory.set(nameBytes.subarray(0, n), ptr);
|
|
448
|
+
return 0;
|
|
449
|
+
},
|
|
450
|
+
fd_read(fd, iovsPtr, iovsLen, nreadPtr) {
|
|
451
|
+
if (debug.trace) console.log(`[wasi] fd_read fd=${fd} iovsLen=${iovsLen}`);
|
|
452
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
453
|
+
const memory = new Uint8Array(instance.exports.memory.buffer);
|
|
454
|
+
let total;
|
|
455
|
+
if (fd === 0) {
|
|
456
|
+
total = readFromStdin(io, view, memory, iovsPtr, iovsLen);
|
|
457
|
+
} else {
|
|
458
|
+
const f = openFiles.get(fd);
|
|
459
|
+
if (!f || f.type !== "file") return E_BADF;
|
|
460
|
+
total = readFromOpenFile(io, f, view, memory, iovsPtr, iovsLen);
|
|
461
|
+
if (total < 0) return E_BADF;
|
|
462
|
+
}
|
|
463
|
+
view.setUint32(nreadPtr, total, true);
|
|
464
|
+
return 0;
|
|
465
|
+
},
|
|
466
|
+
fd_readdir(fd, bufPtr, bufLen, cookie, bufusedPtr) {
|
|
467
|
+
const f = openFiles.get(fd);
|
|
468
|
+
if (!f || f.type !== "dir") return E_BADF;
|
|
469
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
470
|
+
const memory = new Uint8Array(instance.exports.memory.buffer);
|
|
471
|
+
const realNames = Object.keys(f.node.entries);
|
|
472
|
+
const all = [".", "..", ...realNames];
|
|
473
|
+
let bufPos = 0;
|
|
474
|
+
for (let i = Number(cookie); i < all.length; i++) {
|
|
475
|
+
const name = all[i];
|
|
476
|
+
const nameBytes = encoder.encode(name);
|
|
477
|
+
const recordSize = 24 + nameBytes.length;
|
|
478
|
+
if (bufPos + recordSize > bufLen) break;
|
|
479
|
+
for (let j = 0; j < 24; j++) view.setUint8(bufPtr + bufPos + j, 0);
|
|
480
|
+
view.setBigUint64(bufPtr + bufPos, BigInt(i + 1), true);
|
|
481
|
+
view.setBigUint64(bufPtr + bufPos + 8, BigInt(i + 1), true);
|
|
482
|
+
view.setUint32(bufPtr + bufPos + 16, nameBytes.length, true);
|
|
483
|
+
const child = i < 2 ? f.node : f.node.entries[name];
|
|
484
|
+
view.setUint8(bufPtr + bufPos + 20,
|
|
485
|
+
child instanceof Directory ? FILETYPE_DIRECTORY : FILETYPE_REGULAR_FILE);
|
|
486
|
+
memory.set(nameBytes, bufPtr + bufPos + 24);
|
|
487
|
+
bufPos += recordSize;
|
|
488
|
+
}
|
|
489
|
+
view.setUint32(bufusedPtr, bufPos, true);
|
|
490
|
+
return 0;
|
|
491
|
+
},
|
|
492
|
+
path_open(dirfd, _dirflags, pathPtr, pathLen,
|
|
493
|
+
oflags, _rightsBase, _rightsInh, fdflags, fdPtr) {
|
|
494
|
+
if (debug.trace) console.log(`[wasi] path_open dirfd=${dirfd} path="${readUtf8(pathPtr, pathLen)}" oflags=${oflags} fdflags=${fdflags}`);
|
|
495
|
+
if (dirfd !== PREOPEN_FD) return E_BADF;
|
|
496
|
+
const fullPath = resolvePathToAbs(readUtf8(pathPtr, pathLen));
|
|
497
|
+
const directory = !!(oflags & O_DIRECTORY);
|
|
498
|
+
const create = !!(oflags & O_CREAT);
|
|
499
|
+
const excl = !!(oflags & O_EXCL);
|
|
500
|
+
const trunc = !!(oflags & O_TRUNC);
|
|
501
|
+
const append = !!(fdflags & FD_APPEND);
|
|
502
|
+
const node = lookupNode(root, fullPath);
|
|
503
|
+
|
|
504
|
+
if (directory) {
|
|
505
|
+
if (!node) return E_NOENT;
|
|
506
|
+
if (!(node instanceof Directory)) return E_NOTDIR;
|
|
507
|
+
const fd = nextFileFd++;
|
|
508
|
+
openFiles.set(fd, { type: "dir", node });
|
|
509
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
510
|
+
view.setUint32(fdPtr, fd, true);
|
|
511
|
+
return 0;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
if (node instanceof Directory) return E_ISDIR;
|
|
515
|
+
const exists = node instanceof File;
|
|
516
|
+
if (excl && exists) return E_EXIST;
|
|
517
|
+
if (!exists && !create) return E_NOENT;
|
|
518
|
+
if (!exists || trunc) fs.set(fullPath, new Uint8Array(0));
|
|
519
|
+
|
|
520
|
+
const data = fs.get(fullPath);
|
|
521
|
+
const fd = nextFileFd++;
|
|
522
|
+
openFiles.set(fd, { type: "file", path: fullPath, pos: append ? data.length : 0, append });
|
|
523
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
524
|
+
view.setUint32(fdPtr, fd, true);
|
|
525
|
+
return 0;
|
|
526
|
+
},
|
|
527
|
+
path_filestat_get(dirfd, _flags, pathPtr, pathLen, ptr) {
|
|
528
|
+
let baseDir;
|
|
529
|
+
if (dirfd === PREOPEN_FD) {
|
|
530
|
+
baseDir = root;
|
|
531
|
+
} else {
|
|
532
|
+
const f = openFiles.get(dirfd);
|
|
533
|
+
if (!f || f.type !== "dir") return E_BADF;
|
|
534
|
+
baseDir = f.node;
|
|
535
|
+
}
|
|
536
|
+
const node = resolveRelative(baseDir, readUtf8(pathPtr, pathLen));
|
|
537
|
+
if (!node) return E_NOENT;
|
|
538
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
539
|
+
if (node instanceof Directory) writeFilestat(view, ptr, FILETYPE_DIRECTORY, 0);
|
|
540
|
+
else writeFilestat(view, ptr, FILETYPE_REGULAR_FILE, node.data.length);
|
|
541
|
+
return 0;
|
|
542
|
+
},
|
|
543
|
+
proc_exit(code) {
|
|
544
|
+
console.log("[mruby] proc_exit", code);
|
|
545
|
+
},
|
|
546
|
+
environ_sizes_get(countPtr, sizesPtr) {
|
|
547
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
548
|
+
const entries = Object.entries(env);
|
|
549
|
+
let totalSize = 0;
|
|
550
|
+
for (const [k, v] of entries) totalSize += encoder.encode(`${k}=${v}\0`).length;
|
|
551
|
+
view.setUint32(countPtr, entries.length, true);
|
|
552
|
+
view.setUint32(sizesPtr, totalSize, true);
|
|
553
|
+
return 0;
|
|
554
|
+
},
|
|
555
|
+
environ_get(envPtr, bufPtr) {
|
|
556
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
557
|
+
const memory = new Uint8Array(instance.exports.memory.buffer);
|
|
558
|
+
let offset = bufPtr;
|
|
559
|
+
Object.entries(env).forEach(([k, v], i) => {
|
|
560
|
+
view.setUint32(envPtr + i * 4, offset, true);
|
|
561
|
+
const bytes = encoder.encode(`${k}=${v}\0`);
|
|
562
|
+
memory.set(bytes, offset);
|
|
563
|
+
offset += bytes.length;
|
|
564
|
+
});
|
|
565
|
+
return 0;
|
|
566
|
+
},
|
|
567
|
+
args_sizes_get(countPtr, sizesPtr) {
|
|
568
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
569
|
+
let totalSize = 0;
|
|
570
|
+
for (const a of args) totalSize += encoder.encode(`${a}\0`).length;
|
|
571
|
+
view.setUint32(countPtr, args.length, true);
|
|
572
|
+
view.setUint32(sizesPtr, totalSize, true);
|
|
573
|
+
return 0;
|
|
574
|
+
},
|
|
575
|
+
args_get(argvPtr, bufPtr) {
|
|
576
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
577
|
+
const memory = new Uint8Array(instance.exports.memory.buffer);
|
|
578
|
+
let offset = bufPtr;
|
|
579
|
+
args.forEach((a, i) => {
|
|
580
|
+
view.setUint32(argvPtr + i * 4, offset, true);
|
|
581
|
+
const bytes = encoder.encode(`${a}\0`);
|
|
582
|
+
memory.set(bytes, offset);
|
|
583
|
+
offset += bytes.length;
|
|
584
|
+
});
|
|
585
|
+
return 0;
|
|
586
|
+
},
|
|
587
|
+
clock_time_get(id, _precision, ptr) {
|
|
588
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
589
|
+
let nanos;
|
|
590
|
+
if (id === 0) nanos = BigInt(Math.floor(Date.now() * 1e6));
|
|
591
|
+
else {
|
|
592
|
+
const now = (typeof performance !== "undefined" ? performance.now() : Date.now());
|
|
593
|
+
nanos = BigInt(Math.floor(now * 1e6));
|
|
594
|
+
}
|
|
595
|
+
view.setBigUint64(ptr, nanos, true);
|
|
596
|
+
return 0;
|
|
597
|
+
},
|
|
598
|
+
clock_res_get(_id, ptr) {
|
|
599
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
600
|
+
view.setBigUint64(ptr, 1_000_000n, true);
|
|
601
|
+
return 0;
|
|
602
|
+
},
|
|
603
|
+
random_get(ptr, len) {
|
|
604
|
+
const memory = new Uint8Array(instance.exports.memory.buffer, ptr, len);
|
|
605
|
+
for (let off = 0; off < len; off += 65536) {
|
|
606
|
+
const slice = memory.subarray(off, Math.min(off + 65536, len));
|
|
607
|
+
crypto.getRandomValues(slice);
|
|
608
|
+
}
|
|
609
|
+
return 0;
|
|
610
|
+
},
|
|
611
|
+
fd_filestat_set_size(fd, size) {
|
|
612
|
+
const f = openFiles.get(fd);
|
|
613
|
+
if (!f || f.type !== "file") return E_BADF;
|
|
614
|
+
const data = fs.get(f.path);
|
|
615
|
+
if (!data) return E_BADF;
|
|
616
|
+
const newSize = Number(size);
|
|
617
|
+
if (newSize < 0) return E_INVAL;
|
|
618
|
+
if (newSize < data.length) fs.set(f.path, data.slice(0, newSize));
|
|
619
|
+
else if (newSize > data.length) {
|
|
620
|
+
const grown = new Uint8Array(newSize);
|
|
621
|
+
grown.set(data);
|
|
622
|
+
fs.set(f.path, grown);
|
|
623
|
+
}
|
|
624
|
+
return 0;
|
|
625
|
+
},
|
|
626
|
+
fd_pwrite(fd, iovsPtr, iovsLen, offset, nwrittenPtr) {
|
|
627
|
+
const f = openFiles.get(fd);
|
|
628
|
+
if (!f || f.type !== "file") return E_BADF;
|
|
629
|
+
const view = new DataView(instance.exports.memory.buffer);
|
|
630
|
+
const memory = new Uint8Array(instance.exports.memory.buffer);
|
|
631
|
+
const pseudo = { path: f.path, pos: Number(offset), append: false };
|
|
632
|
+
const total = writeToOpenFile(io, pseudo, view, memory, iovsPtr, iovsLen);
|
|
633
|
+
view.setUint32(nwrittenPtr, total, true);
|
|
634
|
+
return 0;
|
|
635
|
+
},
|
|
636
|
+
path_unlink_file(dirfd, pathPtr, pathLen) {
|
|
637
|
+
if (dirfd !== PREOPEN_FD) return E_BADF;
|
|
638
|
+
const fullPath = resolvePathToAbs(readUtf8(pathPtr, pathLen));
|
|
639
|
+
const node = lookupNode(root, fullPath);
|
|
640
|
+
if (!node) return E_NOENT;
|
|
641
|
+
if (node instanceof Directory) return E_ISDIR;
|
|
642
|
+
fs.delete(fullPath);
|
|
643
|
+
return 0;
|
|
644
|
+
},
|
|
645
|
+
path_create_directory(dirfd, pathPtr, pathLen) {
|
|
646
|
+
if (dirfd !== PREOPEN_FD) return E_BADF;
|
|
647
|
+
const fullPath = resolvePathToAbs(readUtf8(pathPtr, pathLen));
|
|
648
|
+
const segs = pathSegments(fullPath);
|
|
649
|
+
if (segs.length === 0) return E_EXIST;
|
|
650
|
+
let dir = root;
|
|
651
|
+
for (let i = 0; i < segs.length - 1; i++) {
|
|
652
|
+
const next = dir.entries[segs[i]];
|
|
653
|
+
if (next == null) return E_NOENT;
|
|
654
|
+
if (!(next instanceof Directory)) return E_NOTDIR;
|
|
655
|
+
dir = next;
|
|
656
|
+
}
|
|
657
|
+
const leaf = segs[segs.length - 1];
|
|
658
|
+
if (dir.entries[leaf] != null) return E_EXIST;
|
|
659
|
+
dir.entries[leaf] = new Directory();
|
|
660
|
+
return 0;
|
|
661
|
+
},
|
|
662
|
+
path_remove_directory(dirfd, pathPtr, pathLen) {
|
|
663
|
+
if (dirfd !== PREOPEN_FD) return E_BADF;
|
|
664
|
+
const fullPath = resolvePathToAbs(readUtf8(pathPtr, pathLen));
|
|
665
|
+
const r = lookupFull(root, fullPath);
|
|
666
|
+
if (!r || r.node == null) return E_NOENT;
|
|
667
|
+
if (!(r.node instanceof Directory)) return E_NOTDIR;
|
|
668
|
+
if (!r.parent) return E_INVAL;
|
|
669
|
+
if (Object.keys(r.node.entries).length > 0) return E_NOTEMPTY;
|
|
670
|
+
delete r.parent.entries[r.name];
|
|
671
|
+
return 0;
|
|
672
|
+
},
|
|
673
|
+
fd_filestat_set_times(_fd, _atim, _mtim, _flags) { return E_INVAL; },
|
|
674
|
+
fd_pread(_fd, _iovs, _iovsLen, _offset, _nreadPtr) { return E_INVAL; },
|
|
675
|
+
fd_renumber(_from, _to) { return E_INVAL; },
|
|
676
|
+
fd_sync(_fd) { return 0; },
|
|
677
|
+
fd_advise(_fd, _offset, _len, _advice) { return 0; },
|
|
678
|
+
fd_allocate(_fd, _offset, _len) { return E_INVAL; },
|
|
679
|
+
fd_datasync(_fd) { return 0; },
|
|
680
|
+
path_filestat_set_times() { return E_INVAL; },
|
|
681
|
+
path_link() { return E_INVAL; },
|
|
682
|
+
path_readlink() { return E_INVAL; },
|
|
683
|
+
path_rename(oldDirfd, oldPathPtr, oldPathLen, newDirfd, newPathPtr, newPathLen) {
|
|
684
|
+
if (oldDirfd !== PREOPEN_FD || newDirfd !== PREOPEN_FD) return E_BADF;
|
|
685
|
+
const oldPath = resolvePathToAbs(readUtf8(oldPathPtr, oldPathLen));
|
|
686
|
+
const newPath = resolvePathToAbs(readUtf8(newPathPtr, newPathLen));
|
|
687
|
+
if (!fs.has(oldPath)) return E_NOENT;
|
|
688
|
+
fs.set(newPath, fs.get(oldPath));
|
|
689
|
+
fs.delete(oldPath);
|
|
690
|
+
return 0;
|
|
691
|
+
},
|
|
692
|
+
path_symlink() { return E_INVAL; },
|
|
693
|
+
poll_oneoff() { return E_INVAL; },
|
|
694
|
+
sched_yield() { return 0; },
|
|
695
|
+
};
|
|
696
|
+
|
|
697
|
+
return {
|
|
698
|
+
imports,
|
|
699
|
+
bindInstance(inst) { instance = inst; },
|
|
700
|
+
env,
|
|
701
|
+
args,
|
|
702
|
+
stdin,
|
|
703
|
+
fs,
|
|
704
|
+
};
|
|
705
|
+
}
|