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