300 lines
13 KiB
JavaScript
300 lines
13 KiB
JavaScript
"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
|