302 lines
12 KiB
JavaScript
302 lines
12 KiB
JavaScript
"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
|