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,461 @@
|
|
|
1
|
+
// JS host adapter for the mruby-wasm-js mrbgem.
|
|
2
|
+
//
|
|
3
|
+
// Provides the JS core via a single factory:
|
|
4
|
+
//
|
|
5
|
+
// import { createVM, Directory, File } from "mruby-wasm-js";
|
|
6
|
+
//
|
|
7
|
+
// const vm = await createVM({
|
|
8
|
+
// wasm: "/path/to/mruby-js.wasm",
|
|
9
|
+
// env: { LOCALE: "ja" },
|
|
10
|
+
// fs: new Directory({ "data": new Directory({ "poem.vtt": new File(bytes) }) }),
|
|
11
|
+
// });
|
|
12
|
+
//
|
|
13
|
+
// vm.eval('puts ENV["LOCALE"]');
|
|
14
|
+
// vm.evalScript('#ruby-source'); // eval textContent of <script id="ruby-source">
|
|
15
|
+
// vm.fs.set("/late.txt", bytes);
|
|
16
|
+
// vm.env["DEBUG"] = "1";
|
|
17
|
+
//
|
|
18
|
+
// Each createVM() call instantiates an independent mruby — separate
|
|
19
|
+
// handle table, separate WASI state. Multiple VMs can coexist in one
|
|
20
|
+
// process (useful for tests, sandboxing, hot reload).
|
|
21
|
+
//
|
|
22
|
+
// Internal layout:
|
|
23
|
+
// - createHandleTable(): per-VM handle table (alloc/get/release/count)
|
|
24
|
+
// - createErrorSlot(): per-VM JS exception capture
|
|
25
|
+
// - inspectValue(v): pure debug-string formatter
|
|
26
|
+
// - createJsImports({…}): builds the 25 js.* methods
|
|
27
|
+
// - createVM(options): orchestrator — creates state,
|
|
28
|
+
// builds imports, instantiates wasm,
|
|
29
|
+
// runs _start, returns VM handle.
|
|
30
|
+
|
|
31
|
+
import { createWasiPreview1, createFsFacade, Directory, File } from "./wasi-preview1.js";
|
|
32
|
+
import { createMemoryHelpers, encoder } from "./_memory.js";
|
|
33
|
+
import { debug } from "./debug.js";
|
|
34
|
+
|
|
35
|
+
export { Directory, File, createFsFacade, debug };
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Thrown by `vm.eval` / `vm.loadBytecode` / `vm.evalScript` when mruby
|
|
39
|
+
* raises an unhandled exception. The fields mirror what mruby itself
|
|
40
|
+
* exposes — `rubyClass` is `exc.class.name`, `backtrace` is what
|
|
41
|
+
* `exc.backtrace` returned (file:line:in method, one entry per frame).
|
|
42
|
+
*
|
|
43
|
+
* Stack is best-effort: the JS engine's own stack trace shows where
|
|
44
|
+
* `vm.eval` was called from; the Ruby backtrace is the real one.
|
|
45
|
+
*/
|
|
46
|
+
export class RubyError extends Error {
|
|
47
|
+
constructor({ class: rubyClass, message, backtrace } = {}) {
|
|
48
|
+
super(message || rubyClass || "mruby exception");
|
|
49
|
+
this.name = "RubyError";
|
|
50
|
+
this.rubyClass = rubyClass || "Exception";
|
|
51
|
+
this.backtrace = Array.isArray(backtrace) ? backtrace : [];
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// `env` import object for instantiateStreaming. Empty in current builds:
|
|
56
|
+
// the gem's mruby-js.wasm uses hal-wasi-io (mrbgem/hal-wasi-io/) for the
|
|
57
|
+
// IO HAL backend, and mruby-wasi-stubs (mrbgem/mruby-wasi-stubs/) for
|
|
58
|
+
// the few POSIX symbols mruby-io's io.c references directly (dup,
|
|
59
|
+
// waitpid). Both are linked into the wasm itself, leaving the `env`
|
|
60
|
+
// import module with nothing to satisfy.
|
|
61
|
+
const envImports = {};
|
|
62
|
+
|
|
63
|
+
// --- Pure helpers ---------------------------------------------------------
|
|
64
|
+
|
|
65
|
+
// Best-effort debug string for a JS value. JSON for plain objects so
|
|
66
|
+
// `p value` shows structure; tag DOM nodes / functions specially since
|
|
67
|
+
// JSON.stringify drops them.
|
|
68
|
+
function inspectValue(v) {
|
|
69
|
+
if (v === null) return "null";
|
|
70
|
+
if (v === undefined) return "undefined";
|
|
71
|
+
const t = typeof v;
|
|
72
|
+
if (t === "string") return JSON.stringify(v);
|
|
73
|
+
if (t === "number" || t === "boolean") return String(v);
|
|
74
|
+
if (t === "function") return `#<JS function ${v.name || "(anonymous)"}>`;
|
|
75
|
+
if (t === "symbol") return v.toString();
|
|
76
|
+
if (v && typeof v.nodeType === "number" && typeof v.nodeName === "string") {
|
|
77
|
+
return `#<JS ${v.nodeName.toLowerCase()}${v.id ? ` id=${JSON.stringify(v.id)}` : ""}>`;
|
|
78
|
+
}
|
|
79
|
+
try { return JSON.stringify(v); }
|
|
80
|
+
catch (_err) { return `#<JS ${Object.prototype.toString.call(v)}>`; }
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// --- Per-VM state factories -----------------------------------------------
|
|
84
|
+
|
|
85
|
+
/** Per-VM handle table. Index 0 is reserved as a "null" sentinel.
|
|
86
|
+
* Allocations recycle from a free list to keep handle numbers small. */
|
|
87
|
+
function createHandleTable() {
|
|
88
|
+
const handles = [null];
|
|
89
|
+
const free = [];
|
|
90
|
+
return {
|
|
91
|
+
alloc(value) {
|
|
92
|
+
if (free.length > 0) {
|
|
93
|
+
const h = free.pop();
|
|
94
|
+
handles[h] = value;
|
|
95
|
+
return h;
|
|
96
|
+
}
|
|
97
|
+
handles.push(value);
|
|
98
|
+
return handles.length - 1;
|
|
99
|
+
},
|
|
100
|
+
get(h) { return handles[h]; },
|
|
101
|
+
release(h) {
|
|
102
|
+
if (h === 0) return;
|
|
103
|
+
if (handles[h] === null) return;
|
|
104
|
+
handles[h] = null;
|
|
105
|
+
free.push(h);
|
|
106
|
+
},
|
|
107
|
+
count() { return handles.length - 1 - free.length; },
|
|
108
|
+
isNull(h) { return h === 0 || handles[h] == null; },
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/** Per-VM "latest JS exception" slot. The C side calls js_take_error()
|
|
113
|
+
* right after each potentially-throwing op; if a non-null Error is
|
|
114
|
+
* pending, it becomes a JS::Error on the Ruby side. Non-Error
|
|
115
|
+
* throws (`throw "string"`, `throw 42`, ...) are wrapped so callers
|
|
116
|
+
* always get an object with `.message`. */
|
|
117
|
+
function createErrorSlot() {
|
|
118
|
+
let pending = null;
|
|
119
|
+
return {
|
|
120
|
+
capture(err) { pending = err; },
|
|
121
|
+
take() {
|
|
122
|
+
if (pending == null) return null;
|
|
123
|
+
let err = pending;
|
|
124
|
+
pending = null;
|
|
125
|
+
if (!(err instanceof Error)) err = new Error(String(err));
|
|
126
|
+
return err;
|
|
127
|
+
},
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/** Build the 25 `js.*` import methods, closing over the supplied
|
|
132
|
+
* per-VM state. Splitting this out from createVM means the imports can
|
|
133
|
+
* be unit-tested or rebuilt independently of the wasm fetch/instantiate
|
|
134
|
+
* cycle. */
|
|
135
|
+
function createJsImports({ handles, errorSlot, getInstance }) {
|
|
136
|
+
const { readUtf8, writeUtf8, readHandleArray } = createMemoryHelpers(getInstance);
|
|
137
|
+
return {
|
|
138
|
+
// Evaluate JS source and return a handle to the resulting value.
|
|
139
|
+
// NOTE: uses `Function` constructor for simplicity; not a sandbox.
|
|
140
|
+
js_eval(ptr, len) {
|
|
141
|
+
const src = readUtf8(ptr, len);
|
|
142
|
+
let result;
|
|
143
|
+
try { result = new Function(`return (${src});`)(); }
|
|
144
|
+
catch (err) { errorSlot.capture(err); return 0; }
|
|
145
|
+
return handles.alloc(result);
|
|
146
|
+
},
|
|
147
|
+
js_global() { return handles.alloc(globalThis); },
|
|
148
|
+
js_release(h) {
|
|
149
|
+
if (debug.trace && h !== 0 && handles.get(h) !== null) {
|
|
150
|
+
console.log(`[trace] js_release h=${h} (was ${typeof handles.get(h)})`);
|
|
151
|
+
}
|
|
152
|
+
handles.release(h);
|
|
153
|
+
},
|
|
154
|
+
js_get(h, keyPtr, keyLen) {
|
|
155
|
+
const key = readUtf8(keyPtr, keyLen);
|
|
156
|
+
const obj = handles.get(h);
|
|
157
|
+
if (obj == null) {
|
|
158
|
+
errorSlot.capture(new TypeError(`cannot read property '${key}' of ${obj}`));
|
|
159
|
+
return 0;
|
|
160
|
+
}
|
|
161
|
+
try { return handles.alloc(obj[key]); }
|
|
162
|
+
catch (err) { errorSlot.capture(err); return 0; }
|
|
163
|
+
},
|
|
164
|
+
js_set(h, keyPtr, keyLen, valueHandle) {
|
|
165
|
+
const key = readUtf8(keyPtr, keyLen);
|
|
166
|
+
const obj = handles.get(h);
|
|
167
|
+
if (obj == null) {
|
|
168
|
+
errorSlot.capture(new TypeError(`cannot set property '${key}' of ${obj}`));
|
|
169
|
+
return;
|
|
170
|
+
}
|
|
171
|
+
try { obj[key] = handles.get(valueHandle); }
|
|
172
|
+
catch (err) { errorSlot.capture(err); }
|
|
173
|
+
},
|
|
174
|
+
js_call(h, methodPtr, methodLen, argsPtr, argCount) {
|
|
175
|
+
const method = readUtf8(methodPtr, methodLen);
|
|
176
|
+
const obj = handles.get(h);
|
|
177
|
+
if (obj == null) {
|
|
178
|
+
errorSlot.capture(new TypeError(`cannot call '${method}' on ${obj}`));
|
|
179
|
+
return 0;
|
|
180
|
+
}
|
|
181
|
+
const argHandles = readHandleArray(argsPtr, argCount);
|
|
182
|
+
const args = argHandles.map((a) => handles.get(a));
|
|
183
|
+
try { return handles.alloc(obj[method].apply(obj, args)); }
|
|
184
|
+
catch (err) { errorSlot.capture(err); return 0; }
|
|
185
|
+
},
|
|
186
|
+
js_new(h, argsPtr, argCount) {
|
|
187
|
+
const ctor = handles.get(h);
|
|
188
|
+
if (typeof ctor !== "function") {
|
|
189
|
+
errorSlot.capture(new TypeError(`handle ${h} is not a constructor`));
|
|
190
|
+
return 0;
|
|
191
|
+
}
|
|
192
|
+
const argHandles = readHandleArray(argsPtr, argCount);
|
|
193
|
+
const args = argHandles.map((a) => handles.get(a));
|
|
194
|
+
try { return handles.alloc(new ctor(...args)); }
|
|
195
|
+
catch (err) { errorSlot.capture(err); return 0; }
|
|
196
|
+
},
|
|
197
|
+
js_handle_count() { return handles.count(); },
|
|
198
|
+
js_take_error() {
|
|
199
|
+
const err = errorSlot.take();
|
|
200
|
+
return err == null ? 0 : handles.alloc(err);
|
|
201
|
+
},
|
|
202
|
+
js_to_string_len(h) {
|
|
203
|
+
const v = handles.get(h);
|
|
204
|
+
return v == null ? 0 : encoder.encode(String(v)).length;
|
|
205
|
+
},
|
|
206
|
+
js_to_string_copy(h, ptr, bufLen) {
|
|
207
|
+
const v = handles.get(h);
|
|
208
|
+
if (v == null) return;
|
|
209
|
+
writeUtf8(String(v), ptr, bufLen);
|
|
210
|
+
},
|
|
211
|
+
js_from_string(ptr, len) { return handles.alloc(readUtf8(ptr, len)); },
|
|
212
|
+
js_to_int(h) {
|
|
213
|
+
const v = handles.get(h);
|
|
214
|
+
return v == null ? 0 : (v | 0);
|
|
215
|
+
},
|
|
216
|
+
js_from_int(v) { return handles.alloc(v); },
|
|
217
|
+
js_to_float(h) {
|
|
218
|
+
const v = handles.get(h);
|
|
219
|
+
return v == null ? 0 : Number(v);
|
|
220
|
+
},
|
|
221
|
+
js_from_float(v) { return handles.alloc(v); },
|
|
222
|
+
js_is_null(h) { return handles.isNull(h) ? 1 : 0; },
|
|
223
|
+
js_strict_equal(a, b) { return handles.get(a) === handles.get(b) ? 1 : 0; },
|
|
224
|
+
js_typeof_len(h) { return encoder.encode(typeof handles.get(h)).length; },
|
|
225
|
+
js_typeof_copy(h, ptr, bufLen) { writeUtf8(typeof handles.get(h), ptr, bufLen); },
|
|
226
|
+
js_inspect_len(h) { return encoder.encode(inspectValue(handles.get(h))).length; },
|
|
227
|
+
js_inspect_copy(h, ptr, bufLen) { writeUtf8(inspectValue(handles.get(h)), ptr, bufLen); },
|
|
228
|
+
js_instanceof(instanceH, ctorH) {
|
|
229
|
+
const ctor = handles.get(ctorH);
|
|
230
|
+
if (typeof ctor !== "function") return 0;
|
|
231
|
+
try { return handles.get(instanceH) instanceof ctor ? 1 : 0; }
|
|
232
|
+
catch (_err) { return 0; }
|
|
233
|
+
},
|
|
234
|
+
js_make_callback(callbackId) {
|
|
235
|
+
const wrapper = (...args) => {
|
|
236
|
+
if (debug.trace) console.log(`[trace] wrapper id=${callbackId} fired with`, args);
|
|
237
|
+
const argsHandle = handles.alloc(args);
|
|
238
|
+
try {
|
|
239
|
+
// js_invoke_proc returns a fresh handle for the block's return
|
|
240
|
+
// value (0 = undefined). We own it, so read + release.
|
|
241
|
+
const resultHandle = getInstance().exports.js_invoke_proc(callbackId, argsHandle);
|
|
242
|
+
if (resultHandle === 0) return undefined;
|
|
243
|
+
const result = handles.get(resultHandle);
|
|
244
|
+
handles.release(resultHandle);
|
|
245
|
+
return result;
|
|
246
|
+
} finally { handles.release(argsHandle); }
|
|
247
|
+
};
|
|
248
|
+
return handles.alloc(wrapper);
|
|
249
|
+
},
|
|
250
|
+
js_clone(h) {
|
|
251
|
+
// Fresh handle pointing at the same JS value — used so JS-bound
|
|
252
|
+
// copies don't share ownership with Ruby's original handle.
|
|
253
|
+
if (h === 0) return 0;
|
|
254
|
+
return handles.alloc(handles.get(h));
|
|
255
|
+
},
|
|
256
|
+
};
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
// --- Public factory -------------------------------------------------------
|
|
260
|
+
|
|
261
|
+
/**
|
|
262
|
+
* Instantiate a fresh mruby VM and return a handle for driving it.
|
|
263
|
+
*
|
|
264
|
+
* @param {object} options
|
|
265
|
+
* @param {string} options.wasm URL to mruby-js.wasm
|
|
266
|
+
* @param {Record<string, string>} [options.env] initial ENV
|
|
267
|
+
* @param {string[]} [options.args] initial ARGV (defaults to ["mruby-wasm-js"])
|
|
268
|
+
* @param {string|Uint8Array} [options.stdin] initial stdin payload
|
|
269
|
+
* @param {Directory} [options.fs] initial root Directory for the VFS
|
|
270
|
+
* @param {object} [options.wasi] replacement `wasi_snapshot_preview1` import object;
|
|
271
|
+
* defaults to a fresh in-memory WASI preview1 impl
|
|
272
|
+
* @param {(instance: WebAssembly.Instance) => void} [options.onStart]
|
|
273
|
+
* called once after instantiation; defaults to
|
|
274
|
+
* calling `instance.exports._start()`
|
|
275
|
+
*
|
|
276
|
+
* @returns {Promise<{
|
|
277
|
+
* instance: WebAssembly.Instance,
|
|
278
|
+
* eval: (source: string) => number, // 0 on success, 1 on parse/runtime error. Throws NotImplementedError in compiler-less builds.
|
|
279
|
+
* loadBytecode: (bytes: Uint8Array | ArrayBuffer) => number, // load pre-compiled mrbc bytecode. 0 on success, 1 on runtime error.
|
|
280
|
+
* alloc: (value: any) => number, // power-user handle table
|
|
281
|
+
* get: (handle: number) => any,
|
|
282
|
+
* release: (handle: number) => void,
|
|
283
|
+
* handleCount: () => number,
|
|
284
|
+
* fs?: object, // present iff bundled WASI is in use
|
|
285
|
+
* env?: Record<string, string>, // present iff bundled WASI is in use
|
|
286
|
+
* args?: string[], // present iff bundled WASI is in use
|
|
287
|
+
* stdin?: { bytes: Uint8Array, pushText: (s: string) => void }, // present iff bundled WASI is in use
|
|
288
|
+
* }>}
|
|
289
|
+
*
|
|
290
|
+
* Note: when `options.wasi` is provided, the returned VM does NOT include
|
|
291
|
+
* `fs` / `env` / `args` / `stdin` — those keys describe the bundled
|
|
292
|
+
* WASI preview1's state, and the caller's WASI replacement owns its own
|
|
293
|
+
* state instead. This keeps the typed surface of the returned object
|
|
294
|
+
* consistent with which WASI is actually backing it.
|
|
295
|
+
*
|
|
296
|
+
* Swap WASI for `@bjorn3/browser_wasi_shim`:
|
|
297
|
+
*
|
|
298
|
+
* import { WASI } from "@bjorn3/browser_wasi_shim";
|
|
299
|
+
* const wasi = new WASI([], [], preopens);
|
|
300
|
+
* const vm = await createVM({
|
|
301
|
+
* wasm: "/path/to/mruby-js.wasm",
|
|
302
|
+
* wasi: wasi.wasiImport,
|
|
303
|
+
* onStart: (instance) => wasi.start(instance),
|
|
304
|
+
* });
|
|
305
|
+
*
|
|
306
|
+
* The `js.*` imports (the JS layer itself) are always
|
|
307
|
+
* provided by this adapter regardless of which WASI is used.
|
|
308
|
+
*/
|
|
309
|
+
export async function createVM(options = {}) {
|
|
310
|
+
const { wasm, onStart } = options;
|
|
311
|
+
if (!wasm) throw new Error("createVM: options.wasm (URL to mruby-js.wasm) is required");
|
|
312
|
+
|
|
313
|
+
const handles = createHandleTable();
|
|
314
|
+
const errorSlot = createErrorSlot();
|
|
315
|
+
let instance = null;
|
|
316
|
+
const getInstance = () => instance;
|
|
317
|
+
|
|
318
|
+
const jsImports = createJsImports({ handles, errorSlot, getInstance });
|
|
319
|
+
|
|
320
|
+
const customWasi = options.wasi;
|
|
321
|
+
const wasiImpl = customWasi ? null : createWasiPreview1({
|
|
322
|
+
env: options.env,
|
|
323
|
+
args: options.args,
|
|
324
|
+
stdin: options.stdin,
|
|
325
|
+
fs: options.fs,
|
|
326
|
+
});
|
|
327
|
+
const wasiImports = customWasi ?? wasiImpl.imports;
|
|
328
|
+
|
|
329
|
+
const response = await fetch(wasm);
|
|
330
|
+
if (!response.ok) {
|
|
331
|
+
throw new Error(`createVM: failed to fetch ${wasm}: ${response.status}`);
|
|
332
|
+
}
|
|
333
|
+
const result = await WebAssembly.instantiateStreaming(response, {
|
|
334
|
+
env: envImports,
|
|
335
|
+
js: jsImports,
|
|
336
|
+
wasi_snapshot_preview1: wasiImports,
|
|
337
|
+
});
|
|
338
|
+
instance = result.instance;
|
|
339
|
+
if (wasiImpl) wasiImpl.bindInstance(instance);
|
|
340
|
+
|
|
341
|
+
if (onStart) {
|
|
342
|
+
onStart(instance);
|
|
343
|
+
} else if (typeof instance.exports._initialize === "function") {
|
|
344
|
+
// Reactor module: runs ctors (including the gem's mrb_open + ARGV
|
|
345
|
+
// boot ctor) and returns. No exit-pseudo-exception to catch.
|
|
346
|
+
instance.exports._initialize();
|
|
347
|
+
} else if (typeof instance.exports._start === "function") {
|
|
348
|
+
// Command module fallback (e.g. caller-supplied wasm built without
|
|
349
|
+
// -mexec-model=reactor). _start can throw a pseudo-exception on exit;
|
|
350
|
+
// swallow that one path and let real errors propagate.
|
|
351
|
+
try { instance.exports._start(); }
|
|
352
|
+
catch (err) {
|
|
353
|
+
if (err.message && !err.message.includes("exit")) throw err;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Pull the structured exception left by the previous failing eval /
|
|
358
|
+
// loadBytecode and surface it as a RubyError. The wasm side stashes a
|
|
359
|
+
// JS object handle in g_last_error_handle when mrb->exc is set; we
|
|
360
|
+
// drain + release it here. Returns null when no error is pending
|
|
361
|
+
// (e.g., a parse fail before mruby ever got to raise).
|
|
362
|
+
function takeRubyError() {
|
|
363
|
+
const h = instance.exports.js_take_last_error();
|
|
364
|
+
if (!h) return null;
|
|
365
|
+
const info = handles.get(h);
|
|
366
|
+
handles.release(h);
|
|
367
|
+
return info ? new RubyError(info) : null;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
function evalRuby(source, options = {}) {
|
|
371
|
+
const { filename, lineOffset = 0, throw: shouldThrow = true } = options;
|
|
372
|
+
const srcH = handles.alloc(source);
|
|
373
|
+
const fileH = filename ? handles.alloc(String(filename)) : 0;
|
|
374
|
+
let rc;
|
|
375
|
+
try {
|
|
376
|
+
rc = instance.exports.js_eval_handle(srcH, fileH, lineOffset | 0);
|
|
377
|
+
} finally {
|
|
378
|
+
handles.release(srcH);
|
|
379
|
+
if (fileH) handles.release(fileH);
|
|
380
|
+
}
|
|
381
|
+
// rc === 2: compiler-less build signalled that source eval is not
|
|
382
|
+
// available. Surface as NotImplementedError so the caller learns to
|
|
383
|
+
// pre-compile with mrbc and use loadBytecode instead.
|
|
384
|
+
if (rc === 2) {
|
|
385
|
+
const err = new Error(
|
|
386
|
+
"vm.eval(source) is not available in this mruby build " +
|
|
387
|
+
"(compiled without mruby-compiler). Pre-compile with mrbc and use vm.loadBytecode(bytes) instead.",
|
|
388
|
+
);
|
|
389
|
+
err.name = "NotImplementedError";
|
|
390
|
+
throw err;
|
|
391
|
+
}
|
|
392
|
+
if (rc !== 0 && shouldThrow) {
|
|
393
|
+
const err = takeRubyError();
|
|
394
|
+
if (err) throw err;
|
|
395
|
+
throw new Error("mruby eval failed with no structured error info");
|
|
396
|
+
}
|
|
397
|
+
// throw: false branch — keep the legacy rc=0/1 contract and let the
|
|
398
|
+
// caller decide whether to inspect the (now-drained) error slot.
|
|
399
|
+
if (!shouldThrow && rc !== 0) takeRubyError();
|
|
400
|
+
return rc;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Load pre-compiled mruby bytecode (output of `mrbc`). The bytes must
|
|
404
|
+
// already contain whatever fiber wrapping the source needs — this path
|
|
405
|
+
// does NOT auto-wrap, unlike `eval(source)`. Available in all build
|
|
406
|
+
// variants; primary use is the compiler-less / production variant.
|
|
407
|
+
//
|
|
408
|
+
// Accepts `Uint8Array` or `ArrayBuffer` (auto-wrapped as a zero-copy
|
|
409
|
+
// view), since `await fetch(...).arrayBuffer()` returns the latter.
|
|
410
|
+
function loadBytecode(bytes, options = {}) {
|
|
411
|
+
if (bytes instanceof ArrayBuffer) bytes = new Uint8Array(bytes);
|
|
412
|
+
if (!(bytes instanceof Uint8Array)) {
|
|
413
|
+
throw new TypeError("loadBytecode: expected Uint8Array or ArrayBuffer");
|
|
414
|
+
}
|
|
415
|
+
const { throw: shouldThrow = true } = options;
|
|
416
|
+
const handle = handles.alloc(bytes);
|
|
417
|
+
let rc;
|
|
418
|
+
try { rc = instance.exports.js_load_irep_handle(handle); }
|
|
419
|
+
finally { handles.release(handle); }
|
|
420
|
+
if (rc !== 0 && shouldThrow) {
|
|
421
|
+
const err = takeRubyError();
|
|
422
|
+
if (err) throw err;
|
|
423
|
+
throw new Error("mruby loadBytecode failed with no structured error info");
|
|
424
|
+
}
|
|
425
|
+
if (!shouldThrow && rc !== 0) takeRubyError();
|
|
426
|
+
return rc;
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
// Eval the textContent of a DOM element matched by `selector`.
|
|
430
|
+
// Pairs with `<script type="text/ruby">` blocks. Browser-only.
|
|
431
|
+
function evalScript(selector, options = {}) {
|
|
432
|
+
if (typeof document === "undefined") {
|
|
433
|
+
throw new Error("evalScript: requires a DOM (document is undefined)");
|
|
434
|
+
}
|
|
435
|
+
const el = document.querySelector(selector);
|
|
436
|
+
if (!el) throw new Error(`evalScript: no element matches ${JSON.stringify(selector)}`);
|
|
437
|
+
return evalRuby(el.textContent, options);
|
|
438
|
+
}
|
|
439
|
+
|
|
440
|
+
// Core VM surface plus, when we own the WASI side, the bundled VFS
|
|
441
|
+
// state (fs / env / args / stdin). Keys are omitted entirely when
|
|
442
|
+
// the caller passed their own `wasi` — that object controls fs/env/
|
|
443
|
+
// args/stdin, and `undefined` placeholders are harder to typecheck
|
|
444
|
+
// and easier to misread than absent properties.
|
|
445
|
+
return {
|
|
446
|
+
instance,
|
|
447
|
+
eval: evalRuby,
|
|
448
|
+
loadBytecode,
|
|
449
|
+
evalScript,
|
|
450
|
+
alloc: handles.alloc,
|
|
451
|
+
get: handles.get,
|
|
452
|
+
release: handles.release,
|
|
453
|
+
handleCount: () => handles.count(),
|
|
454
|
+
...(wasiImpl && {
|
|
455
|
+
fs: wasiImpl.fs,
|
|
456
|
+
env: wasiImpl.env,
|
|
457
|
+
args: wasiImpl.args,
|
|
458
|
+
stdin: wasiImpl.stdin,
|
|
459
|
+
}),
|
|
460
|
+
};
|
|
461
|
+
}
|