runtime.js 13 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.QuickJSRuntime = void 0;
  4. const asyncify_helpers_1 = require("./asyncify-helpers");
  5. const context_1 = require("./context");
  6. const debug_1 = require("./debug");
  7. const errors_1 = require("./errors");
  8. const lifetime_1 = require("./lifetime");
  9. const memory_1 = require("./memory");
  10. const types_1 = require("./types");
  11. /**
  12. * A runtime represents a Javascript runtime corresponding to an object heap.
  13. * Several runtimes can exist at the same time but they cannot exchange objects.
  14. * Inside a given runtime, no multi-threading is supported.
  15. *
  16. * You can think of separate runtimes like different domains in a browser, and
  17. * the contexts within a runtime like the different windows open to the same
  18. * domain.
  19. *
  20. * Create a runtime via {@link QuickJSWASMModule.newRuntime}.
  21. *
  22. * You should create separate runtime instances for untrusted code from
  23. * different sources for isolation. However, stronger isolation is also
  24. * available (at the cost of memory usage), by creating separate WebAssembly
  25. * modules to further isolate untrusted code.
  26. * See {@link newQuickJSWASMModule}.
  27. *
  28. * Implement memory and CPU constraints with [[setInterruptHandler]]
  29. * (called regularly while the interpreter runs), [[setMemoryLimit]], and
  30. * [[setMaxStackSize]].
  31. * Use [[computeMemoryUsage]] or [[dumpMemoryUsage]] to guide memory limit
  32. * tuning.
  33. *
  34. * Configure ES module loading with [[setModuleLoader]].
  35. */
  36. class QuickJSRuntime {
  37. /** @private */
  38. constructor(args) {
  39. /** @private */
  40. this.scope = new lifetime_1.Scope();
  41. /** @private */
  42. this.contextMap = new Map();
  43. this.cToHostCallbacks = {
  44. shouldInterrupt: (rt) => {
  45. if (rt !== this.rt.value) {
  46. throw new Error("QuickJSContext instance received C -> JS interrupt with mismatched rt");
  47. }
  48. const fn = this.interruptHandler;
  49. if (!fn) {
  50. throw new Error("QuickJSContext had no interrupt handler");
  51. }
  52. return fn(this) ? 1 : 0;
  53. },
  54. loadModuleSource: (0, asyncify_helpers_1.maybeAsyncFn)(this, function* (awaited, rt, ctx, moduleName) {
  55. const moduleLoader = this.moduleLoader;
  56. if (!moduleLoader) {
  57. throw new Error("Runtime has no module loader");
  58. }
  59. if (rt !== this.rt.value) {
  60. throw new Error("Runtime pointer mismatch");
  61. }
  62. const context = this.contextMap.get(ctx) ??
  63. this.newContext({
  64. contextPointer: ctx,
  65. });
  66. try {
  67. const result = yield* awaited(moduleLoader(moduleName, context));
  68. if (typeof result === "object" && "error" in result && result.error) {
  69. (0, debug_1.debugLog)("cToHostLoadModule: loader returned error", result.error);
  70. throw result.error;
  71. }
  72. const moduleSource = typeof result === "string" ? result : "value" in result ? result.value : result;
  73. return this.memory.newHeapCharPointer(moduleSource).value;
  74. }
  75. catch (error) {
  76. (0, debug_1.debugLog)("cToHostLoadModule: caught error", error);
  77. context.throw(error);
  78. return 0;
  79. }
  80. }),
  81. normalizeModule: (0, asyncify_helpers_1.maybeAsyncFn)(this, function* (awaited, rt, ctx, baseModuleName, moduleNameRequest) {
  82. const moduleNormalizer = this.moduleNormalizer;
  83. if (!moduleNormalizer) {
  84. throw new Error("Runtime has no module normalizer");
  85. }
  86. if (rt !== this.rt.value) {
  87. throw new Error("Runtime pointer mismatch");
  88. }
  89. const context = this.contextMap.get(ctx) ??
  90. this.newContext({
  91. /* TODO: Does this happen? Are we responsible for disposing? I don't think so */
  92. contextPointer: ctx,
  93. });
  94. try {
  95. const result = yield* awaited(moduleNormalizer(baseModuleName, moduleNameRequest, context));
  96. if (typeof result === "object" && "error" in result && result.error) {
  97. (0, debug_1.debugLog)("cToHostNormalizeModule: normalizer returned error", result.error);
  98. throw result.error;
  99. }
  100. const name = typeof result === "string" ? result : result.value;
  101. return context.getMemory(this.rt.value).newHeapCharPointer(name).value;
  102. }
  103. catch (error) {
  104. (0, debug_1.debugLog)("normalizeModule: caught error", error);
  105. context.throw(error);
  106. return 0;
  107. }
  108. }),
  109. };
  110. args.ownedLifetimes?.forEach((lifetime) => this.scope.manage(lifetime));
  111. this.module = args.module;
  112. this.memory = new memory_1.ModuleMemory(this.module);
  113. this.ffi = args.ffi;
  114. this.rt = args.rt;
  115. this.callbacks = args.callbacks;
  116. this.scope.manage(this.rt);
  117. this.callbacks.setRuntimeCallbacks(this.rt.value, this.cToHostCallbacks);
  118. this.executePendingJobs = this.executePendingJobs.bind(this);
  119. }
  120. get alive() {
  121. return this.scope.alive;
  122. }
  123. dispose() {
  124. return this.scope.dispose();
  125. }
  126. newContext(options = {}) {
  127. if (options.intrinsics && options.intrinsics !== types_1.DefaultIntrinsics) {
  128. throw new Error("TODO: Custom intrinsics are not supported yet");
  129. }
  130. const ctx = new lifetime_1.Lifetime(options.contextPointer || this.ffi.QTS_NewContext(this.rt.value), undefined, (ctx_ptr) => {
  131. this.contextMap.delete(ctx_ptr);
  132. this.callbacks.deleteContext(ctx_ptr);
  133. this.ffi.QTS_FreeContext(ctx_ptr);
  134. });
  135. const context = new context_1.QuickJSContext({
  136. module: this.module,
  137. ctx,
  138. ffi: this.ffi,
  139. rt: this.rt,
  140. ownedLifetimes: options.ownedLifetimes,
  141. runtime: this,
  142. callbacks: this.callbacks,
  143. });
  144. this.contextMap.set(ctx.value, context);
  145. return context;
  146. }
  147. /**
  148. * Set the loader for EcmaScript modules requested by any context in this
  149. * runtime.
  150. *
  151. * The loader can be removed with [[removeModuleLoader]].
  152. */
  153. setModuleLoader(moduleLoader, moduleNormalizer) {
  154. this.moduleLoader = moduleLoader;
  155. this.moduleNormalizer = moduleNormalizer;
  156. this.ffi.QTS_RuntimeEnableModuleLoader(this.rt.value, this.moduleNormalizer ? 1 : 0);
  157. }
  158. /**
  159. * Remove the the loader set by [[setModuleLoader]]. This disables module loading.
  160. */
  161. removeModuleLoader() {
  162. this.moduleLoader = undefined;
  163. this.ffi.QTS_RuntimeDisableModuleLoader(this.rt.value);
  164. }
  165. // Runtime management -------------------------------------------------------
  166. /**
  167. * In QuickJS, promises and async functions create pendingJobs. These do not execute
  168. * immediately and need to be run by calling [[executePendingJobs]].
  169. *
  170. * @return true if there is at least one pendingJob queued up.
  171. */
  172. hasPendingJob() {
  173. return Boolean(this.ffi.QTS_IsJobPending(this.rt.value));
  174. }
  175. /**
  176. * Set a callback which is regularly called by the QuickJS engine when it is
  177. * executing code. This callback can be used to implement an execution
  178. * timeout.
  179. *
  180. * The interrupt handler can be removed with [[removeInterruptHandler]].
  181. */
  182. setInterruptHandler(cb) {
  183. const prevInterruptHandler = this.interruptHandler;
  184. this.interruptHandler = cb;
  185. if (!prevInterruptHandler) {
  186. this.ffi.QTS_RuntimeEnableInterruptHandler(this.rt.value);
  187. }
  188. }
  189. /**
  190. * Remove the interrupt handler, if any.
  191. * See [[setInterruptHandler]].
  192. */
  193. removeInterruptHandler() {
  194. if (this.interruptHandler) {
  195. this.ffi.QTS_RuntimeDisableInterruptHandler(this.rt.value);
  196. this.interruptHandler = undefined;
  197. }
  198. }
  199. /**
  200. * Execute pendingJobs on the runtime until `maxJobsToExecute` jobs are
  201. * executed (default all pendingJobs), the queue is exhausted, or the runtime
  202. * encounters an exception.
  203. *
  204. * In QuickJS, promises and async functions *inside the runtime* create
  205. * pendingJobs. These do not execute immediately and need to triggered to run.
  206. *
  207. * @param maxJobsToExecute - When negative, run all pending jobs. Otherwise execute
  208. * at most `maxJobsToExecute` before returning.
  209. *
  210. * @return On success, the number of executed jobs. On error, the exception
  211. * that stopped execution, and the context it occurred in. Note that
  212. * executePendingJobs will not normally return errors thrown inside async
  213. * functions or rejected promises. Those errors are available by calling
  214. * [[resolvePromise]] on the promise handle returned by the async function.
  215. */
  216. executePendingJobs(maxJobsToExecute = -1) {
  217. const ctxPtrOut = this.memory.newMutablePointerArray(1);
  218. const valuePtr = this.ffi.QTS_ExecutePendingJob(this.rt.value, maxJobsToExecute ?? -1, ctxPtrOut.value.ptr);
  219. const ctxPtr = ctxPtrOut.value.typedArray[0];
  220. ctxPtrOut.dispose();
  221. if (ctxPtr === 0) {
  222. // No jobs executed.
  223. this.ffi.QTS_FreeValuePointerRuntime(this.rt.value, valuePtr);
  224. return { value: 0 };
  225. }
  226. const context = this.contextMap.get(ctxPtr) ??
  227. this.newContext({
  228. contextPointer: ctxPtr,
  229. });
  230. const resultValue = context.getMemory(this.rt.value).heapValueHandle(valuePtr);
  231. const typeOfRet = context.typeof(resultValue);
  232. if (typeOfRet === "number") {
  233. const executedJobs = context.getNumber(resultValue);
  234. resultValue.dispose();
  235. return { value: executedJobs };
  236. }
  237. else {
  238. const error = Object.assign(resultValue, { context });
  239. return {
  240. error,
  241. };
  242. }
  243. }
  244. /**
  245. * Set the max memory this runtime can allocate.
  246. * To remove the limit, set to `-1`.
  247. */
  248. setMemoryLimit(limitBytes) {
  249. if (limitBytes < 0 && limitBytes !== -1) {
  250. throw new Error("Cannot set memory limit to negative number. To unset, pass -1");
  251. }
  252. this.ffi.QTS_RuntimeSetMemoryLimit(this.rt.value, limitBytes);
  253. }
  254. /**
  255. * Compute memory usage for this runtime. Returns the result as a handle to a
  256. * JSValue object. Use [[QuickJSContext.dump]] to convert to a native object.
  257. * Calling this method will allocate more memory inside the runtime. The information
  258. * is accurate as of just before the call to `computeMemoryUsage`.
  259. * For a human-digestible representation, see [[dumpMemoryUsage]].
  260. */
  261. computeMemoryUsage() {
  262. const serviceContextMemory = this.getSystemContext().getMemory(this.rt.value);
  263. return serviceContextMemory.heapValueHandle(this.ffi.QTS_RuntimeComputeMemoryUsage(this.rt.value, serviceContextMemory.ctx.value));
  264. }
  265. /**
  266. * @returns a human-readable description of memory usage in this runtime.
  267. * For programmatic access to this information, see [[computeMemoryUsage]].
  268. */
  269. dumpMemoryUsage() {
  270. return this.memory.consumeHeapCharPointer(this.ffi.QTS_RuntimeDumpMemoryUsage(this.rt.value));
  271. }
  272. /**
  273. * Set the max stack size for this runtime, in bytes.
  274. * To remove the limit, set to `0`.
  275. */
  276. setMaxStackSize(stackSize) {
  277. if (stackSize < 0) {
  278. throw new Error("Cannot set memory limit to negative number. To unset, pass 0.");
  279. }
  280. this.ffi.QTS_RuntimeSetMaxStackSize(this.rt.value, stackSize);
  281. }
  282. /**
  283. * Assert that `handle` is owned by this runtime.
  284. * @throws QuickJSWrongOwner if owned by a different runtime.
  285. */
  286. assertOwned(handle) {
  287. if (handle.owner && handle.owner.rt !== this.rt) {
  288. throw new errors_1.QuickJSWrongOwner(`Handle is not owned by this runtime: ${handle.owner.rt.value} != ${this.rt.value}`);
  289. }
  290. }
  291. getSystemContext() {
  292. if (!this.context) {
  293. // We own this context and should dispose of it.
  294. this.context = this.scope.manage(this.newContext());
  295. }
  296. return this.context;
  297. }
  298. }
  299. exports.QuickJSRuntime = QuickJSRuntime;
  300. //# sourceMappingURL=runtime.js.map