"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.QuickJSRuntime = void 0; const asyncify_helpers_1 = require("./asyncify-helpers"); const context_1 = require("./context"); const debug_1 = require("./debug"); const errors_1 = require("./errors"); const lifetime_1 = require("./lifetime"); const memory_1 = require("./memory"); const types_1 = require("./types"); /** * A runtime represents a Javascript runtime corresponding to an object heap. * Several runtimes can exist at the same time but they cannot exchange objects. * Inside a given runtime, no multi-threading is supported. * * You can think of separate runtimes like different domains in a browser, and * the contexts within a runtime like the different windows open to the same * domain. * * Create a runtime via {@link QuickJSWASMModule.newRuntime}. * * You should create separate runtime instances for untrusted code from * different sources for isolation. However, stronger isolation is also * available (at the cost of memory usage), by creating separate WebAssembly * modules to further isolate untrusted code. * See {@link newQuickJSWASMModule}. * * Implement memory and CPU constraints with [[setInterruptHandler]] * (called regularly while the interpreter runs), [[setMemoryLimit]], and * [[setMaxStackSize]]. * Use [[computeMemoryUsage]] or [[dumpMemoryUsage]] to guide memory limit * tuning. * * Configure ES module loading with [[setModuleLoader]]. */ class QuickJSRuntime { /** @private */ constructor(args) { /** @private */ this.scope = new lifetime_1.Scope(); /** @private */ this.contextMap = new Map(); this.cToHostCallbacks = { shouldInterrupt: (rt) => { if (rt !== this.rt.value) { throw new Error("QuickJSContext instance received C -> JS interrupt with mismatched rt"); } const fn = this.interruptHandler; if (!fn) { throw new Error("QuickJSContext had no interrupt handler"); } return fn(this) ? 1 : 0; }, loadModuleSource: (0, asyncify_helpers_1.maybeAsyncFn)(this, function* (awaited, rt, ctx, moduleName) { const moduleLoader = this.moduleLoader; if (!moduleLoader) { throw new Error("Runtime has no module loader"); } if (rt !== this.rt.value) { throw new Error("Runtime pointer mismatch"); } const context = this.contextMap.get(ctx) ?? this.newContext({ contextPointer: ctx, }); try { const result = yield* awaited(moduleLoader(moduleName, context)); if (typeof result === "object" && "error" in result && result.error) { (0, debug_1.debugLog)("cToHostLoadModule: loader returned error", result.error); throw result.error; } const moduleSource = typeof result === "string" ? result : "value" in result ? result.value : result; return this.memory.newHeapCharPointer(moduleSource).value; } catch (error) { (0, debug_1.debugLog)("cToHostLoadModule: caught error", error); context.throw(error); return 0; } }), normalizeModule: (0, asyncify_helpers_1.maybeAsyncFn)(this, function* (awaited, rt, ctx, baseModuleName, moduleNameRequest) { const moduleNormalizer = this.moduleNormalizer; if (!moduleNormalizer) { throw new Error("Runtime has no module normalizer"); } if (rt !== this.rt.value) { throw new Error("Runtime pointer mismatch"); } const context = this.contextMap.get(ctx) ?? this.newContext({ /* TODO: Does this happen? Are we responsible for disposing? I don't think so */ contextPointer: ctx, }); try { const result = yield* awaited(moduleNormalizer(baseModuleName, moduleNameRequest, context)); if (typeof result === "object" && "error" in result && result.error) { (0, debug_1.debugLog)("cToHostNormalizeModule: normalizer returned error", result.error); throw result.error; } const name = typeof result === "string" ? result : result.value; return context.getMemory(this.rt.value).newHeapCharPointer(name).value; } catch (error) { (0, debug_1.debugLog)("normalizeModule: caught error", error); context.throw(error); return 0; } }), }; args.ownedLifetimes?.forEach((lifetime) => this.scope.manage(lifetime)); this.module = args.module; this.memory = new memory_1.ModuleMemory(this.module); this.ffi = args.ffi; this.rt = args.rt; this.callbacks = args.callbacks; this.scope.manage(this.rt); this.callbacks.setRuntimeCallbacks(this.rt.value, this.cToHostCallbacks); this.executePendingJobs = this.executePendingJobs.bind(this); } get alive() { return this.scope.alive; } dispose() { return this.scope.dispose(); } newContext(options = {}) { if (options.intrinsics && options.intrinsics !== types_1.DefaultIntrinsics) { throw new Error("TODO: Custom intrinsics are not supported yet"); } const ctx = new lifetime_1.Lifetime(options.contextPointer || this.ffi.QTS_NewContext(this.rt.value), undefined, (ctx_ptr) => { this.contextMap.delete(ctx_ptr); this.callbacks.deleteContext(ctx_ptr); this.ffi.QTS_FreeContext(ctx_ptr); }); const context = new context_1.QuickJSContext({ module: this.module, ctx, ffi: this.ffi, rt: this.rt, ownedLifetimes: options.ownedLifetimes, runtime: this, callbacks: this.callbacks, }); this.contextMap.set(ctx.value, context); return context; } /** * Set the loader for EcmaScript modules requested by any context in this * runtime. * * The loader can be removed with [[removeModuleLoader]]. */ setModuleLoader(moduleLoader, moduleNormalizer) { this.moduleLoader = moduleLoader; this.moduleNormalizer = moduleNormalizer; this.ffi.QTS_RuntimeEnableModuleLoader(this.rt.value, this.moduleNormalizer ? 1 : 0); } /** * Remove the the loader set by [[setModuleLoader]]. This disables module loading. */ removeModuleLoader() { this.moduleLoader = undefined; this.ffi.QTS_RuntimeDisableModuleLoader(this.rt.value); } // Runtime management ------------------------------------------------------- /** * In QuickJS, promises and async functions create pendingJobs. These do not execute * immediately and need to be run by calling [[executePendingJobs]]. * * @return true if there is at least one pendingJob queued up. */ hasPendingJob() { return Boolean(this.ffi.QTS_IsJobPending(this.rt.value)); } /** * Set a callback which is regularly called by the QuickJS engine when it is * executing code. This callback can be used to implement an execution * timeout. * * The interrupt handler can be removed with [[removeInterruptHandler]]. */ setInterruptHandler(cb) { const prevInterruptHandler = this.interruptHandler; this.interruptHandler = cb; if (!prevInterruptHandler) { this.ffi.QTS_RuntimeEnableInterruptHandler(this.rt.value); } } /** * Remove the interrupt handler, if any. * See [[setInterruptHandler]]. */ removeInterruptHandler() { if (this.interruptHandler) { this.ffi.QTS_RuntimeDisableInterruptHandler(this.rt.value); this.interruptHandler = undefined; } } /** * Execute pendingJobs on the runtime until `maxJobsToExecute` jobs are * executed (default all pendingJobs), the queue is exhausted, or the runtime * encounters an exception. * * In QuickJS, promises and async functions *inside the runtime* create * pendingJobs. These do not execute immediately and need to triggered to run. * * @param maxJobsToExecute - When negative, run all pending jobs. Otherwise execute * at most `maxJobsToExecute` before returning. * * @return On success, the number of executed jobs. On error, the exception * that stopped execution, and the context it occurred in. Note that * executePendingJobs will not normally return errors thrown inside async * functions or rejected promises. Those errors are available by calling * [[resolvePromise]] on the promise handle returned by the async function. */ executePendingJobs(maxJobsToExecute = -1) { const ctxPtrOut = this.memory.newMutablePointerArray(1); const valuePtr = this.ffi.QTS_ExecutePendingJob(this.rt.value, maxJobsToExecute ?? -1, ctxPtrOut.value.ptr); const ctxPtr = ctxPtrOut.value.typedArray[0]; ctxPtrOut.dispose(); if (ctxPtr === 0) { // No jobs executed. this.ffi.QTS_FreeValuePointerRuntime(this.rt.value, valuePtr); return { value: 0 }; } const context = this.contextMap.get(ctxPtr) ?? this.newContext({ contextPointer: ctxPtr, }); const resultValue = context.getMemory(this.rt.value).heapValueHandle(valuePtr); const typeOfRet = context.typeof(resultValue); if (typeOfRet === "number") { const executedJobs = context.getNumber(resultValue); resultValue.dispose(); return { value: executedJobs }; } else { const error = Object.assign(resultValue, { context }); return { error, }; } } /** * Set the max memory this runtime can allocate. * To remove the limit, set to `-1`. */ setMemoryLimit(limitBytes) { if (limitBytes < 0 && limitBytes !== -1) { throw new Error("Cannot set memory limit to negative number. To unset, pass -1"); } this.ffi.QTS_RuntimeSetMemoryLimit(this.rt.value, limitBytes); } /** * Compute memory usage for this runtime. Returns the result as a handle to a * JSValue object. Use [[QuickJSContext.dump]] to convert to a native object. * Calling this method will allocate more memory inside the runtime. The information * is accurate as of just before the call to `computeMemoryUsage`. * For a human-digestible representation, see [[dumpMemoryUsage]]. */ computeMemoryUsage() { const serviceContextMemory = this.getSystemContext().getMemory(this.rt.value); return serviceContextMemory.heapValueHandle(this.ffi.QTS_RuntimeComputeMemoryUsage(this.rt.value, serviceContextMemory.ctx.value)); } /** * @returns a human-readable description of memory usage in this runtime. * For programmatic access to this information, see [[computeMemoryUsage]]. */ dumpMemoryUsage() { return this.memory.consumeHeapCharPointer(this.ffi.QTS_RuntimeDumpMemoryUsage(this.rt.value)); } /** * Set the max stack size for this runtime, in bytes. * To remove the limit, set to `0`. */ setMaxStackSize(stackSize) { if (stackSize < 0) { throw new Error("Cannot set memory limit to negative number. To unset, pass 0."); } this.ffi.QTS_RuntimeSetMaxStackSize(this.rt.value, stackSize); } /** * Assert that `handle` is owned by this runtime. * @throws QuickJSWrongOwner if owned by a different runtime. */ assertOwned(handle) { if (handle.owner && handle.owner.rt !== this.rt) { throw new errors_1.QuickJSWrongOwner(`Handle is not owned by this runtime: ${handle.owner.rt.value} != ${this.rt.value}`); } } getSystemContext() { if (!this.context) { // We own this context and should dispose of it. this.context = this.scope.manage(this.newContext()); } return this.context; } } exports.QuickJSRuntime = QuickJSRuntime; //# sourceMappingURL=runtime.js.map