"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 |