module.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302
  1. "use strict";
  2. Object.defineProperty(exports, "__esModule", { value: true });
  3. exports.QuickJSWASMModule = exports.applyModuleEvalRuntimeOptions = exports.applyBaseRuntimeOptions = exports.QuickJSModuleCallbacks = void 0;
  4. const debug_1 = require("./debug");
  5. const errors_1 = require("./errors");
  6. const lifetime_1 = require("./lifetime");
  7. const runtime_1 = require("./runtime");
  8. const types_1 = require("./types");
  9. class QuickJSEmscriptenModuleCallbacks {
  10. constructor(args) {
  11. this.callFunction = args.callFunction;
  12. this.shouldInterrupt = args.shouldInterrupt;
  13. this.loadModuleSource = args.loadModuleSource;
  14. this.normalizeModule = args.normalizeModule;
  15. }
  16. }
  17. /**
  18. * We use static functions per module to dispatch runtime or context calls from
  19. * C to the host. This class manages the indirection from a specific runtime or
  20. * context pointer to the appropriate callback handler.
  21. *
  22. * @private
  23. */
  24. class QuickJSModuleCallbacks {
  25. constructor(module) {
  26. this.contextCallbacks = new Map();
  27. this.runtimeCallbacks = new Map();
  28. this.suspendedCount = 0;
  29. this.cToHostCallbacks = new QuickJSEmscriptenModuleCallbacks({
  30. callFunction: (asyncify, ctx, this_ptr, argc, argv, fn_id) => this.handleAsyncify(asyncify, () => {
  31. try {
  32. const vm = this.contextCallbacks.get(ctx);
  33. if (!vm) {
  34. throw new Error(`QuickJSContext(ctx = ${ctx}) not found for C function call "${fn_id}"`);
  35. }
  36. return vm.callFunction(ctx, this_ptr, argc, argv, fn_id);
  37. }
  38. catch (error) {
  39. console.error("[C to host error: returning null]", error);
  40. return 0;
  41. }
  42. }),
  43. shouldInterrupt: (asyncify, rt) => this.handleAsyncify(asyncify, () => {
  44. try {
  45. const vm = this.runtimeCallbacks.get(rt);
  46. if (!vm) {
  47. throw new Error(`QuickJSRuntime(rt = ${rt}) not found for C interrupt`);
  48. }
  49. return vm.shouldInterrupt(rt);
  50. }
  51. catch (error) {
  52. console.error("[C to host interrupt: returning error]", error);
  53. return 1;
  54. }
  55. }),
  56. loadModuleSource: (asyncify, rt, ctx, moduleName) => this.handleAsyncify(asyncify, () => {
  57. try {
  58. const runtimeCallbacks = this.runtimeCallbacks.get(rt);
  59. if (!runtimeCallbacks) {
  60. throw new Error(`QuickJSRuntime(rt = ${rt}) not found for C module loader`);
  61. }
  62. const loadModule = runtimeCallbacks.loadModuleSource;
  63. if (!loadModule) {
  64. throw new Error(`QuickJSRuntime(rt = ${rt}) does not support module loading`);
  65. }
  66. return loadModule(rt, ctx, moduleName);
  67. }
  68. catch (error) {
  69. console.error("[C to host module loader error: returning null]", error);
  70. return 0;
  71. }
  72. }),
  73. normalizeModule: (asyncify, rt, ctx, moduleBaseName, moduleName) => this.handleAsyncify(asyncify, () => {
  74. try {
  75. const runtimeCallbacks = this.runtimeCallbacks.get(rt);
  76. if (!runtimeCallbacks) {
  77. throw new Error(`QuickJSRuntime(rt = ${rt}) not found for C module loader`);
  78. }
  79. const normalizeModule = runtimeCallbacks.normalizeModule;
  80. if (!normalizeModule) {
  81. throw new Error(`QuickJSRuntime(rt = ${rt}) does not support module loading`);
  82. }
  83. return normalizeModule(rt, ctx, moduleBaseName, moduleName);
  84. }
  85. catch (error) {
  86. console.error("[C to host module loader error: returning null]", error);
  87. return 0;
  88. }
  89. }),
  90. });
  91. this.module = module;
  92. this.module.callbacks = this.cToHostCallbacks;
  93. }
  94. setRuntimeCallbacks(rt, callbacks) {
  95. this.runtimeCallbacks.set(rt, callbacks);
  96. }
  97. deleteRuntime(rt) {
  98. this.runtimeCallbacks.delete(rt);
  99. }
  100. setContextCallbacks(ctx, callbacks) {
  101. this.contextCallbacks.set(ctx, callbacks);
  102. }
  103. deleteContext(ctx) {
  104. this.contextCallbacks.delete(ctx);
  105. }
  106. handleAsyncify(asyncify, fn) {
  107. if (asyncify) {
  108. // We must always call asyncify.handleSync around our function.
  109. // This allows asyncify to resume suspended execution on the second call.
  110. // Asyncify internally can detect sync behavior, and avoid suspending.
  111. return asyncify.handleSleep((done) => {
  112. try {
  113. const result = fn();
  114. if (!(result instanceof Promise)) {
  115. (0, debug_1.debugLog)("asyncify.handleSleep: not suspending:", result);
  116. done(result);
  117. return;
  118. }
  119. // Is promise, we intend to suspend.
  120. if (this.suspended) {
  121. throw new errors_1.QuickJSAsyncifyError(`Already suspended at: ${this.suspended.stack}\nAttempted to suspend at:`);
  122. }
  123. else {
  124. this.suspended = new errors_1.QuickJSAsyncifySuspended(`(${this.suspendedCount++})`);
  125. (0, debug_1.debugLog)("asyncify.handleSleep: suspending:", this.suspended);
  126. }
  127. result.then((resolvedResult) => {
  128. this.suspended = undefined;
  129. (0, debug_1.debugLog)("asyncify.handleSleep: resolved:", resolvedResult);
  130. done(resolvedResult);
  131. }, (error) => {
  132. (0, debug_1.debugLog)("asyncify.handleSleep: rejected:", error);
  133. console.error("QuickJS: cannot handle error in suspended function", error);
  134. this.suspended = undefined;
  135. });
  136. }
  137. catch (error) {
  138. (0, debug_1.debugLog)("asyncify.handleSleep: error:", error);
  139. this.suspended = undefined;
  140. throw error;
  141. }
  142. });
  143. }
  144. // No asyncify - we should never return a promise.
  145. const value = fn();
  146. if (value instanceof Promise) {
  147. throw new Error("Promise return value not supported in non-asyncify context.");
  148. }
  149. return value;
  150. }
  151. }
  152. exports.QuickJSModuleCallbacks = QuickJSModuleCallbacks;
  153. /**
  154. * Process RuntimeOptions and apply them to a QuickJSRuntime.
  155. * @private
  156. */
  157. function applyBaseRuntimeOptions(runtime, options) {
  158. if (options.interruptHandler) {
  159. runtime.setInterruptHandler(options.interruptHandler);
  160. }
  161. if (options.maxStackSizeBytes !== undefined) {
  162. runtime.setMaxStackSize(options.maxStackSizeBytes);
  163. }
  164. if (options.memoryLimitBytes !== undefined) {
  165. runtime.setMemoryLimit(options.memoryLimitBytes);
  166. }
  167. }
  168. exports.applyBaseRuntimeOptions = applyBaseRuntimeOptions;
  169. /**
  170. * Process ModuleEvalOptions and apply them to a QuickJSRuntime.
  171. * @private
  172. */
  173. function applyModuleEvalRuntimeOptions(runtime, options) {
  174. if (options.moduleLoader) {
  175. runtime.setModuleLoader(options.moduleLoader);
  176. }
  177. if (options.shouldInterrupt) {
  178. runtime.setInterruptHandler(options.shouldInterrupt);
  179. }
  180. if (options.memoryLimitBytes !== undefined) {
  181. runtime.setMemoryLimit(options.memoryLimitBytes);
  182. }
  183. if (options.maxStackSizeBytes !== undefined) {
  184. runtime.setMaxStackSize(options.maxStackSizeBytes);
  185. }
  186. }
  187. exports.applyModuleEvalRuntimeOptions = applyModuleEvalRuntimeOptions;
  188. /**
  189. * This class presents a Javascript interface to QuickJS, a Javascript interpreter
  190. * that supports EcmaScript 2020 (ES2020).
  191. *
  192. * It wraps a single WebAssembly module containing the QuickJS library and
  193. * associated helper C code. WebAssembly modules are completely isolated from
  194. * each other by the host's WebAssembly runtime. Separate WebAssembly modules
  195. * have the most isolation guarantees possible with this library.
  196. *
  197. * The simplest way to start running code is {@link evalCode}. This shortcut
  198. * method will evaluate Javascript safely and return the result as a native
  199. * Javascript value.
  200. *
  201. * For more control over the execution environment, or to interact with values
  202. * inside QuickJS, create a context with {@link newContext} or a runtime with
  203. * {@link newRuntime}.
  204. */
  205. class QuickJSWASMModule {
  206. /** @private */
  207. constructor(module, ffi) {
  208. this.module = module;
  209. this.ffi = ffi;
  210. this.callbacks = new QuickJSModuleCallbacks(module);
  211. }
  212. /**
  213. * Create a runtime.
  214. * Use the runtime to set limits on CPU and memory usage and configure module
  215. * loading for one or more [[QuickJSContext]]s inside the runtime.
  216. */
  217. newRuntime(options = {}) {
  218. const rt = new lifetime_1.Lifetime(this.ffi.QTS_NewRuntime(), undefined, (rt_ptr) => {
  219. this.callbacks.deleteRuntime(rt_ptr);
  220. this.ffi.QTS_FreeRuntime(rt_ptr);
  221. });
  222. const runtime = new runtime_1.QuickJSRuntime({
  223. module: this.module,
  224. callbacks: this.callbacks,
  225. ffi: this.ffi,
  226. rt,
  227. });
  228. applyBaseRuntimeOptions(runtime, options);
  229. if (options.moduleLoader) {
  230. runtime.setModuleLoader(options.moduleLoader);
  231. }
  232. return runtime;
  233. }
  234. /**
  235. * A simplified API to create a new [[QuickJSRuntime]] and a
  236. * [[QuickJSContext]] inside that runtime at the same time. The runtime will
  237. * be disposed when the context is disposed.
  238. */
  239. newContext(options = {}) {
  240. const runtime = this.newRuntime();
  241. const context = runtime.newContext({
  242. ...options,
  243. ownedLifetimes: (0, types_1.concat)(runtime, options.ownedLifetimes),
  244. });
  245. runtime.context = context;
  246. return context;
  247. }
  248. /**
  249. * One-off evaluate code without needing to create a [[QuickJSRuntime]] or
  250. * [[QuickJSContext]] explicitly.
  251. *
  252. * To protect against infinite loops, use the `shouldInterrupt` option. The
  253. * [[shouldInterruptAfterDeadline]] function will create a time-based deadline.
  254. *
  255. * If you need more control over how the code executes, create a
  256. * [[QuickJSRuntime]] (with [[newRuntime]]) or a [[QuickJSContext]] (with
  257. * [[newContext]] or [[QuickJSRuntime.newContext]]), and use its
  258. * [[QuickJSContext.evalCode]] method.
  259. *
  260. * Asynchronous callbacks may not run during the first call to `evalCode`. If
  261. * you need to work with async code inside QuickJS, create a runtime and use
  262. * [[QuickJSRuntime.executePendingJobs]].
  263. *
  264. * @returns The result is coerced to a native Javascript value using JSON
  265. * serialization, so properties and values unsupported by JSON will be dropped.
  266. *
  267. * @throws If `code` throws during evaluation, the exception will be
  268. * converted into a native Javascript value and thrown.
  269. *
  270. * @throws if `options.shouldInterrupt` interrupted execution, will throw a Error
  271. * with name `"InternalError"` and message `"interrupted"`.
  272. */
  273. evalCode(code, options = {}) {
  274. return lifetime_1.Scope.withScope((scope) => {
  275. const vm = scope.manage(this.newContext());
  276. applyModuleEvalRuntimeOptions(vm.runtime, options);
  277. const result = vm.evalCode(code, "eval.js");
  278. if (options.memoryLimitBytes !== undefined) {
  279. // Remove memory limit so we can dump the result without exceeding it.
  280. vm.runtime.setMemoryLimit(-1);
  281. }
  282. if (result.error) {
  283. const error = vm.dump(scope.manage(result.error));
  284. throw error;
  285. }
  286. const value = vm.dump(scope.manage(result.value));
  287. return value;
  288. });
  289. }
  290. /**
  291. * Get a low-level interface to the QuickJS functions in this WebAssembly
  292. * module.
  293. * @experimental
  294. * @unstable No warranty is provided with this API. It could change at any time.
  295. * @private
  296. */
  297. getFFI() {
  298. return this.ffi;
  299. }
  300. }
  301. exports.QuickJSWASMModule = QuickJSWASMModule;
  302. //# sourceMappingURL=module.js.map