Newer
Older
vue-indexer / node_modules / @tootallnate / quickjs-emscripten / dist / runtime.js
"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