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