File size: 12,740 Bytes
c2b7eb3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.QuickJSWASMModule = exports.applyModuleEvalRuntimeOptions = exports.applyBaseRuntimeOptions = exports.QuickJSModuleCallbacks = void 0;
const debug_1 = require("./debug");
const errors_1 = require("./errors");
const lifetime_1 = require("./lifetime");
const runtime_1 = require("./runtime");
const types_1 = require("./types");
class QuickJSEmscriptenModuleCallbacks {
    constructor(args) {
        this.callFunction = args.callFunction;
        this.shouldInterrupt = args.shouldInterrupt;
        this.loadModuleSource = args.loadModuleSource;
        this.normalizeModule = args.normalizeModule;
    }
}
/**
 * We use static functions per module to dispatch runtime or context calls from
 * C to the host.  This class manages the indirection from a specific runtime or
 * context pointer to the appropriate callback handler.
 *
 * @private
 */
class QuickJSModuleCallbacks {
    constructor(module) {
        this.contextCallbacks = new Map();
        this.runtimeCallbacks = new Map();
        this.suspendedCount = 0;
        this.cToHostCallbacks = new QuickJSEmscriptenModuleCallbacks({
            callFunction: (asyncify, ctx, this_ptr, argc, argv, fn_id) => this.handleAsyncify(asyncify, () => {
                try {
                    const vm = this.contextCallbacks.get(ctx);
                    if (!vm) {
                        throw new Error(`QuickJSContext(ctx = ${ctx}) not found for C function call "${fn_id}"`);
                    }
                    return vm.callFunction(ctx, this_ptr, argc, argv, fn_id);
                }
                catch (error) {
                    console.error("[C to host error: returning null]", error);
                    return 0;
                }
            }),
            shouldInterrupt: (asyncify, rt) => this.handleAsyncify(asyncify, () => {
                try {
                    const vm = this.runtimeCallbacks.get(rt);
                    if (!vm) {
                        throw new Error(`QuickJSRuntime(rt = ${rt}) not found for C interrupt`);
                    }
                    return vm.shouldInterrupt(rt);
                }
                catch (error) {
                    console.error("[C to host interrupt: returning error]", error);
                    return 1;
                }
            }),
            loadModuleSource: (asyncify, rt, ctx, moduleName) => this.handleAsyncify(asyncify, () => {
                try {
                    const runtimeCallbacks = this.runtimeCallbacks.get(rt);
                    if (!runtimeCallbacks) {
                        throw new Error(`QuickJSRuntime(rt = ${rt}) not found for C module loader`);
                    }
                    const loadModule = runtimeCallbacks.loadModuleSource;
                    if (!loadModule) {
                        throw new Error(`QuickJSRuntime(rt = ${rt}) does not support module loading`);
                    }
                    return loadModule(rt, ctx, moduleName);
                }
                catch (error) {
                    console.error("[C to host module loader error: returning null]", error);
                    return 0;
                }
            }),
            normalizeModule: (asyncify, rt, ctx, moduleBaseName, moduleName) => this.handleAsyncify(asyncify, () => {
                try {
                    const runtimeCallbacks = this.runtimeCallbacks.get(rt);
                    if (!runtimeCallbacks) {
                        throw new Error(`QuickJSRuntime(rt = ${rt}) not found for C module loader`);
                    }
                    const normalizeModule = runtimeCallbacks.normalizeModule;
                    if (!normalizeModule) {
                        throw new Error(`QuickJSRuntime(rt = ${rt}) does not support module loading`);
                    }
                    return normalizeModule(rt, ctx, moduleBaseName, moduleName);
                }
                catch (error) {
                    console.error("[C to host module loader error: returning null]", error);
                    return 0;
                }
            }),
        });
        this.module = module;
        this.module.callbacks = this.cToHostCallbacks;
    }
    setRuntimeCallbacks(rt, callbacks) {
        this.runtimeCallbacks.set(rt, callbacks);
    }
    deleteRuntime(rt) {
        this.runtimeCallbacks.delete(rt);
    }
    setContextCallbacks(ctx, callbacks) {
        this.contextCallbacks.set(ctx, callbacks);
    }
    deleteContext(ctx) {
        this.contextCallbacks.delete(ctx);
    }
    handleAsyncify(asyncify, fn) {
        if (asyncify) {
            // We must always call asyncify.handleSync around our function.
            // This allows asyncify to resume suspended execution on the second call.
            // Asyncify internally can detect sync behavior, and avoid suspending.
            return asyncify.handleSleep((done) => {
                try {
                    const result = fn();
                    if (!(result instanceof Promise)) {
                        (0, debug_1.debugLog)("asyncify.handleSleep: not suspending:", result);
                        done(result);
                        return;
                    }
                    // Is promise, we intend to suspend.
                    if (this.suspended) {
                        throw new errors_1.QuickJSAsyncifyError(`Already suspended at: ${this.suspended.stack}\nAttempted to suspend at:`);
                    }
                    else {
                        this.suspended = new errors_1.QuickJSAsyncifySuspended(`(${this.suspendedCount++})`);
                        (0, debug_1.debugLog)("asyncify.handleSleep: suspending:", this.suspended);
                    }
                    result.then((resolvedResult) => {
                        this.suspended = undefined;
                        (0, debug_1.debugLog)("asyncify.handleSleep: resolved:", resolvedResult);
                        done(resolvedResult);
                    }, (error) => {
                        (0, debug_1.debugLog)("asyncify.handleSleep: rejected:", error);
                        console.error("QuickJS: cannot handle error in suspended function", error);
                        this.suspended = undefined;
                    });
                }
                catch (error) {
                    (0, debug_1.debugLog)("asyncify.handleSleep: error:", error);
                    this.suspended = undefined;
                    throw error;
                }
            });
        }
        // No asyncify - we should never return a promise.
        const value = fn();
        if (value instanceof Promise) {
            throw new Error("Promise return value not supported in non-asyncify context.");
        }
        return value;
    }
}
exports.QuickJSModuleCallbacks = QuickJSModuleCallbacks;
/**
 * Process RuntimeOptions and apply them to a QuickJSRuntime.
 * @private
 */
function applyBaseRuntimeOptions(runtime, options) {
    if (options.interruptHandler) {
        runtime.setInterruptHandler(options.interruptHandler);
    }
    if (options.maxStackSizeBytes !== undefined) {
        runtime.setMaxStackSize(options.maxStackSizeBytes);
    }
    if (options.memoryLimitBytes !== undefined) {
        runtime.setMemoryLimit(options.memoryLimitBytes);
    }
}
exports.applyBaseRuntimeOptions = applyBaseRuntimeOptions;
/**
 * Process ModuleEvalOptions and apply them to a QuickJSRuntime.
 * @private
 */
function applyModuleEvalRuntimeOptions(runtime, options) {
    if (options.moduleLoader) {
        runtime.setModuleLoader(options.moduleLoader);
    }
    if (options.shouldInterrupt) {
        runtime.setInterruptHandler(options.shouldInterrupt);
    }
    if (options.memoryLimitBytes !== undefined) {
        runtime.setMemoryLimit(options.memoryLimitBytes);
    }
    if (options.maxStackSizeBytes !== undefined) {
        runtime.setMaxStackSize(options.maxStackSizeBytes);
    }
}
exports.applyModuleEvalRuntimeOptions = applyModuleEvalRuntimeOptions;
/**
 * This class presents a Javascript interface to QuickJS, a Javascript interpreter
 * that supports EcmaScript 2020 (ES2020).
 *
 * It wraps a single WebAssembly module containing the QuickJS library and
 * associated helper C code. WebAssembly modules are completely isolated from
 * each other by the host's WebAssembly runtime. Separate WebAssembly modules
 * have the most isolation guarantees possible with this library.
 *
 * The simplest way to start running code is {@link evalCode}. This shortcut
 * method will evaluate Javascript safely and return the result as a native
 * Javascript value.
 *
 * For more control over the execution environment, or to interact with values
 * inside QuickJS, create a context with {@link newContext} or a runtime with
 * {@link newRuntime}.
 */
class QuickJSWASMModule {
    /** @private */
    constructor(module, ffi) {
        this.module = module;
        this.ffi = ffi;
        this.callbacks = new QuickJSModuleCallbacks(module);
    }
    /**
     * Create a runtime.
     * Use the runtime to set limits on CPU and memory usage and configure module
     * loading for one or more [[QuickJSContext]]s inside the runtime.
     */
    newRuntime(options = {}) {
        const rt = new lifetime_1.Lifetime(this.ffi.QTS_NewRuntime(), undefined, (rt_ptr) => {
            this.callbacks.deleteRuntime(rt_ptr);
            this.ffi.QTS_FreeRuntime(rt_ptr);
        });
        const runtime = new runtime_1.QuickJSRuntime({
            module: this.module,
            callbacks: this.callbacks,
            ffi: this.ffi,
            rt,
        });
        applyBaseRuntimeOptions(runtime, options);
        if (options.moduleLoader) {
            runtime.setModuleLoader(options.moduleLoader);
        }
        return runtime;
    }
    /**
     * A simplified API to create a new [[QuickJSRuntime]] and a
     * [[QuickJSContext]] inside that runtime at the same time. The runtime will
     * be disposed when the context is disposed.
     */
    newContext(options = {}) {
        const runtime = this.newRuntime();
        const context = runtime.newContext({
            ...options,
            ownedLifetimes: (0, types_1.concat)(runtime, options.ownedLifetimes),
        });
        runtime.context = context;
        return context;
    }
    /**
     * One-off evaluate code without needing to create a [[QuickJSRuntime]] or
     * [[QuickJSContext]] explicitly.
     *
     * To protect against infinite loops, use the `shouldInterrupt` option. The
     * [[shouldInterruptAfterDeadline]] function will create a time-based deadline.
     *
     * If you need more control over how the code executes, create a
     * [[QuickJSRuntime]] (with [[newRuntime]]) or a [[QuickJSContext]] (with
     * [[newContext]] or [[QuickJSRuntime.newContext]]), and use its
     * [[QuickJSContext.evalCode]] method.
     *
     * Asynchronous callbacks may not run during the first call to `evalCode`. If
     * you need to work with async code inside QuickJS, create a runtime and use
     * [[QuickJSRuntime.executePendingJobs]].
     *
     * @returns The result is coerced to a native Javascript value using JSON
     * serialization, so properties and values unsupported by JSON will be dropped.
     *
     * @throws If `code` throws during evaluation, the exception will be
     * converted into a native Javascript value and thrown.
     *
     * @throws if `options.shouldInterrupt` interrupted execution, will throw a Error
     * with name `"InternalError"` and  message `"interrupted"`.
     */
    evalCode(code, options = {}) {
        return lifetime_1.Scope.withScope((scope) => {
            const vm = scope.manage(this.newContext());
            applyModuleEvalRuntimeOptions(vm.runtime, options);
            const result = vm.evalCode(code, "eval.js");
            if (options.memoryLimitBytes !== undefined) {
                // Remove memory limit so we can dump the result without exceeding it.
                vm.runtime.setMemoryLimit(-1);
            }
            if (result.error) {
                const error = vm.dump(scope.manage(result.error));
                throw error;
            }
            const value = vm.dump(scope.manage(result.value));
            return value;
        });
    }
    /**
     * Get a low-level interface to the QuickJS functions in this WebAssembly
     * module.
     * @experimental
     * @unstable No warranty is provided with this API. It could change at any time.
     * @private
     */
    getFFI() {
        return this.ffi;
    }
}
exports.QuickJSWASMModule = QuickJSWASMModule;
//# sourceMappingURL=module.js.map