1 | # quickjs-emscripten
|
2 |
|
3 | Javascript/Typescript bindings for QuickJS, a modern Javascript interpreter,
|
4 | compiled to WebAssembly.
|
5 |
|
6 | - Safely evaluate untrusted Javascript (up to ES2020).
|
7 | - Create and manipulate values inside the QuickJS runtime ([more][values]).
|
8 | - Expose host functions to the QuickJS runtime ([more][functions]).
|
9 | - Execute synchronous code that uses asynchronous functions, with [asyncify][asyncify].
|
10 |
|
11 | [Github] | [NPM] | [API Documentation][api] | [Variants][core] | [Examples][tests]
|
12 |
|
13 | ```typescript
|
14 | import { getQuickJS } from "quickjs-emscripten"
|
15 |
|
16 | async function main() {
|
17 | const QuickJS = await getQuickJS()
|
18 | const vm = QuickJS.newContext()
|
19 |
|
20 | const world = vm.newString("world")
|
21 | vm.setProp(vm.global, "NAME", world)
|
22 | world.dispose()
|
23 |
|
24 | const result = vm.evalCode(`"Hello " + NAME + "!"`)
|
25 | if (result.error) {
|
26 | console.log("Execution failed:", vm.dump(result.error))
|
27 | result.error.dispose()
|
28 | } else {
|
29 | console.log("Success:", vm.dump(result.value))
|
30 | result.value.dispose()
|
31 | }
|
32 |
|
33 | vm.dispose()
|
34 | }
|
35 |
|
36 | main()
|
37 | ```
|
38 |
|
39 | [github]: https://github.com/justjake/quickjs-emscripten
|
40 | [npm]: https://www.npmjs.com/package/quickjs-emscripten
|
41 | [api]: https://github.com/justjake/quickjs-emscripten/blob/main/doc/packages.md
|
42 | [tests]: https://github.com/justjake/quickjs-emscripten/blob/main/packages/quickjs-emscripten/src/quickjs.test.ts
|
43 | [values]: #interfacing-with-the-interpreter
|
44 | [asyncify]: #asyncify
|
45 | [functions]: #exposing-apis
|
46 |
|
47 | - [quickjs-emscripten](#quickjs-emscripten)
|
48 | - [Usage](#usage)
|
49 | - [Safely evaluate Javascript code](#safely-evaluate-javascript-code)
|
50 | - [Interfacing with the interpreter](#interfacing-with-the-interpreter)
|
51 | - [Runtime](#runtime)
|
52 | - [Memory Management](#memory-management)
|
53 | - [Scope](#scope)
|
54 | - [`Lifetime.consume(fn)`](#lifetimeconsumefn)
|
55 | - [Exposing APIs](#exposing-apis)
|
56 | - [Promises](#promises)
|
57 | - [Asyncify](#asyncify)
|
58 | - [Async module loader](#async-module-loader)
|
59 | - [Async on host, sync in QuickJS](#async-on-host-sync-in-quickjs)
|
60 | - [Testing your code](#testing-your-code)
|
61 | - [Using in the browser without a build step](#using-in-the-browser-without-a-build-step)
|
62 | - [quickjs-emscripten-core, variants, and advanced packaging](#quickjs-emscripten-core-variants-and-advanced-packaging)
|
63 | - [Debugging](#debugging)
|
64 | - [More Documentation](#more-documentation)
|
65 | - [Requirements](#requirements)
|
66 | - [Background](#background)
|
67 | - [Status \& Roadmap](#status--roadmap)
|
68 | - [Related](#related)
|
69 | - [Developing](#developing)
|
70 | - [The C parts](#the-c-parts)
|
71 | - [The Typescript parts](#the-typescript-parts)
|
72 | - [Yarn updates](#yarn-updates)
|
73 |
|
74 | ## Usage
|
75 |
|
76 | Install from `npm`: `npm install --save quickjs-emscripten` or `yarn add quickjs-emscripten`.
|
77 |
|
78 | The root entrypoint of this library is the `getQuickJS` function, which returns
|
79 | a promise that resolves to a [QuickJSWASMModule](./doc/quickjs-emscripten/classes/QuickJSWASMModule.md) when
|
80 | the QuickJS WASM module is ready.
|
81 |
|
82 | Once `getQuickJS` has been awaited at least once, you also can use the `getQuickJSSync`
|
83 | function to directly access the singleton in your synchronous code.
|
84 |
|
85 | ### Safely evaluate Javascript code
|
86 |
|
87 | See [QuickJSWASMModule.evalCode](https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten/classes/QuickJSWASMModule.md#evalcode)
|
88 |
|
89 | ```typescript
|
90 | import { getQuickJS, shouldInterruptAfterDeadline } from "quickjs-emscripten"
|
91 |
|
92 | getQuickJS().then((QuickJS) => {
|
93 | const result = QuickJS.evalCode("1 + 1", {
|
94 | shouldInterrupt: shouldInterruptAfterDeadline(Date.now() + 1000),
|
95 | memoryLimitBytes: 1024 * 1024,
|
96 | })
|
97 | console.log(result)
|
98 | })
|
99 | ```
|
100 |
|
101 | ### Interfacing with the interpreter
|
102 |
|
103 | You can use [QuickJSContext](https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten/classes/QuickJSContext.md)
|
104 | to build a scripting environment by modifying globals and exposing functions
|
105 | into the QuickJS interpreter.
|
106 |
|
107 | Each `QuickJSContext` instance has its own environment -- globals, built-in
|
108 | classes -- and actions from one context won't leak into other contexts or
|
109 | runtimes (with one exception, see [Asyncify][asyncify]).
|
110 |
|
111 | Every context is created inside a
|
112 | [QuickJSRuntime](https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten/classes/QuickJSRuntime.md).
|
113 | A runtime represents a Javascript heap, and you can even share values between
|
114 | contexts in the same runtime.
|
115 |
|
116 | ```typescript
|
117 | const vm = QuickJS.newContext()
|
118 | let state = 0
|
119 |
|
120 | const fnHandle = vm.newFunction("nextId", () => {
|
121 | return vm.newNumber(++state)
|
122 | })
|
123 |
|
124 | vm.setProp(vm.global, "nextId", fnHandle)
|
125 | fnHandle.dispose()
|
126 |
|
127 | const nextId = vm.unwrapResult(vm.evalCode(`nextId(); nextId(); nextId()`))
|
128 | console.log("vm result:", vm.getNumber(nextId), "native state:", state)
|
129 |
|
130 | nextId.dispose()
|
131 | vm.dispose()
|
132 | ```
|
133 |
|
134 | When you create a context from a top-level API like in the example above,
|
135 | instead of by calling `runtime.newContext()`, a runtime is automatically created
|
136 | for the lifetime of the context, and disposed of when you dispose the context.
|
137 |
|
138 | #### Runtime
|
139 |
|
140 | The runtime has APIs for CPU and memory limits that apply to all contexts within
|
141 | the runtime in aggregate. You can also use the runtime to configure EcmaScript
|
142 | module loading.
|
143 |
|
144 | ```typescript
|
145 | const runtime = QuickJS.newRuntime()
|
146 | // "Should be enough for everyone" -- attributed to B. Gates
|
147 | runtime.setMemoryLimit(1024 * 640)
|
148 | // Limit stack size
|
149 | runtime.setMaxStackSize(1024 * 320)
|
150 | // Interrupt computation after 1024 calls to the interrupt handler
|
151 | let interruptCycles = 0
|
152 | runtime.setInterruptHandler(() => ++interruptCycles > 1024)
|
153 | // Toy module system that always returns the module name
|
154 | // as the default export
|
155 | runtime.setModuleLoader((moduleName) => `export default '${moduleName}'`)
|
156 | const context = runtime.newContext()
|
157 | const ok = context.evalCode(`
|
158 | import fooName from './foo.js'
|
159 | globalThis.result = fooName
|
160 | `)
|
161 | context.unwrapResult(ok).dispose()
|
162 | // logs "foo.js"
|
163 | console.log(context.getProp(context.global, "result").consume(context.dump))
|
164 | context.dispose()
|
165 | runtime.dispose()
|
166 | ```
|
167 |
|
168 | ### Memory Management
|
169 |
|
170 | Many methods in this library return handles to memory allocated inside the
|
171 | WebAssembly heap. These types cannot be garbage-collected as usual in
|
172 | Javascript. Instead, you must manually manage their memory by calling a
|
173 | `.dispose()` method to free the underlying resources. Once a handle has been
|
174 | disposed, it cannot be used anymore. Note that in the example above, we call
|
175 | `.dispose()` on each handle once it is no longer needed.
|
176 |
|
177 | Calling `QuickJSContext.dispose()` will throw a RuntimeError if you've forgotten to
|
178 | dispose any handles associated with that VM, so it's good practice to create a
|
179 | new VM instance for each of your tests, and to call `vm.dispose()` at the end
|
180 | of every test.
|
181 |
|
182 | ```typescript
|
183 | const vm = QuickJS.newContext()
|
184 | const numberHandle = vm.newNumber(42)
|
185 | // Note: numberHandle not disposed, so it leaks memory.
|
186 | vm.dispose()
|
187 | // throws RuntimeError: abort(Assertion failed: list_empty(&rt->gc_obj_list), at: quickjs/quickjs.c,1963,JS_FreeRuntime)
|
188 | ```
|
189 |
|
190 | Here are some strategies to reduce the toil of calling `.dispose()` on each
|
191 | handle you create:
|
192 |
|
193 | #### Scope
|
194 |
|
195 | A
|
196 | [`Scope`](https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten/classes/Scope.md#class-scope)
|
197 | instance manages a set of disposables and calls their `.dispose()`
|
198 | method in the reverse order in which they're added to the scope. Here's the
|
199 | "Interfacing with the interpreter" example re-written using `Scope`:
|
200 |
|
201 | ```typescript
|
202 | Scope.withScope((scope) => {
|
203 | const vm = scope.manage(QuickJS.newContext())
|
204 | let state = 0
|
205 |
|
206 | const fnHandle = scope.manage(
|
207 | vm.newFunction("nextId", () => {
|
208 | return vm.newNumber(++state)
|
209 | }),
|
210 | )
|
211 |
|
212 | vm.setProp(vm.global, "nextId", fnHandle)
|
213 |
|
214 | const nextId = scope.manage(vm.unwrapResult(vm.evalCode(`nextId(); nextId(); nextId()`)))
|
215 | console.log("vm result:", vm.getNumber(nextId), "native state:", state)
|
216 |
|
217 | // When the withScope block exits, it calls scope.dispose(), which in turn calls
|
218 | // the .dispose() methods of all the disposables managed by the scope.
|
219 | })
|
220 | ```
|
221 |
|
222 | You can also create `Scope` instances with `new Scope()` if you want to manage
|
223 | calling `scope.dispose()` yourself.
|
224 |
|
225 | #### `Lifetime.consume(fn)`
|
226 |
|
227 | [`Lifetime.consume`](https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten/classes/lifetime.md#consume)
|
228 | is sugar for the common pattern of using a handle and then
|
229 | immediately disposing of it. `Lifetime.consume` takes a `map` function that
|
230 | produces a result of any type. The `map` fuction is called with the handle,
|
231 | then the handle is disposed, then the result is returned.
|
232 |
|
233 | Here's the "Interfacing with interpreter" example re-written using `.consume()`:
|
234 |
|
235 | ```typescript
|
236 | const vm = QuickJS.newContext()
|
237 | let state = 0
|
238 |
|
239 | vm.newFunction("nextId", () => {
|
240 | return vm.newNumber(++state)
|
241 | }).consume((fnHandle) => vm.setProp(vm.global, "nextId", fnHandle))
|
242 |
|
243 | vm.unwrapResult(vm.evalCode(`nextId(); nextId(); nextId()`)).consume((nextId) =>
|
244 | console.log("vm result:", vm.getNumber(nextId), "native state:", state),
|
245 | )
|
246 |
|
247 | vm.dispose()
|
248 | ```
|
249 |
|
250 | Generally working with `Scope` leads to more straight-forward code, but
|
251 | `Lifetime.consume` can be handy sugar as part of a method call chain.
|
252 |
|
253 | ### Exposing APIs
|
254 |
|
255 | To add APIs inside the QuickJS environment, you'll need to create objects to
|
256 | define the shape of your API, and add properties and functions to those objects
|
257 | to allow code inside QuickJS to call code on the host.
|
258 |
|
259 | By default, no host functionality is exposed to code running inside QuickJS.
|
260 |
|
261 | ```typescript
|
262 | const vm = QuickJS.newContext()
|
263 | // `console.log`
|
264 | const logHandle = vm.newFunction("log", (...args) => {
|
265 | const nativeArgs = args.map(vm.dump)
|
266 | console.log("QuickJS:", ...nativeArgs)
|
267 | })
|
268 | // Partially implement `console` object
|
269 | const consoleHandle = vm.newObject()
|
270 | vm.setProp(consoleHandle, "log", logHandle)
|
271 | vm.setProp(vm.global, "console", consoleHandle)
|
272 | consoleHandle.dispose()
|
273 | logHandle.dispose()
|
274 |
|
275 | vm.unwrapResult(vm.evalCode(`console.log("Hello from QuickJS!")`)).dispose()
|
276 | ```
|
277 |
|
278 | #### Promises
|
279 |
|
280 | To expose an asynchronous function that _returns a promise_ to callers within
|
281 | QuickJS, your function can return the handle of a `QuickJSDeferredPromise`
|
282 | created via `context.newPromise()`.
|
283 |
|
284 | When you resolve a `QuickJSDeferredPromise` -- and generally whenever async
|
285 | behavior completes for the VM -- pending listeners inside QuickJS may not
|
286 | execute immediately. Your code needs to explicitly call
|
287 | `runtime.executePendingJobs()` to resume execution inside QuickJS. This API
|
288 | gives your code maximum control to _schedule_ when QuickJS will block the host's
|
289 | event loop by resuming execution.
|
290 |
|
291 | To work with QuickJS handles that contain a promise inside the environment, you
|
292 | can convert the QuickJSHandle into a native promise using
|
293 | `context.resolvePromise()`. Take care with this API to avoid 'deadlocks' where
|
294 | the host awaits a guest promise, but the guest cannot make progress until the
|
295 | host calls `runtime.executePendingJobs()`. The simplest way to avoid this kind
|
296 | of deadlock is to always schedule `executePendingJobs` after any promise is
|
297 | settled.
|
298 |
|
299 | ```typescript
|
300 | const vm = QuickJS.newContext()
|
301 | const fakeFileSystem = new Map([["example.txt", "Example file content"]])
|
302 |
|
303 | // Function that simulates reading data asynchronously
|
304 | const readFileHandle = vm.newFunction("readFile", (pathHandle) => {
|
305 | const path = vm.getString(pathHandle)
|
306 | const promise = vm.newPromise()
|
307 | setTimeout(() => {
|
308 | const content = fakeFileSystem.get(path)
|
309 | promise.resolve(vm.newString(content || ""))
|
310 | }, 100)
|
311 | // IMPORTANT: Once you resolve an async action inside QuickJS,
|
312 | // call runtime.executePendingJobs() to run any code that was
|
313 | // waiting on the promise or callback.
|
314 | promise.settled.then(vm.runtime.executePendingJobs)
|
315 | return promise.handle
|
316 | })
|
317 | readFileHandle.consume((handle) => vm.setProp(vm.global, "readFile", handle))
|
318 |
|
319 | // Evaluate code that uses `readFile`, which returns a promise
|
320 | const result = vm.evalCode(`(async () => {
|
321 | const content = await readFile('example.txt')
|
322 | return content.toUpperCase()
|
323 | })()`)
|
324 | const promiseHandle = vm.unwrapResult(result)
|
325 |
|
326 | // Convert the promise handle into a native promise and await it.
|
327 | // If code like this deadlocks, make sure you are calling
|
328 | // runtime.executePendingJobs appropriately.
|
329 | const resolvedResult = await vm.resolvePromise(promiseHandle)
|
330 | promiseHandle.dispose()
|
331 | const resolvedHandle = vm.unwrapResult(resolvedResult)
|
332 | console.log("Result:", vm.getString(resolvedHandle))
|
333 | resolvedHandle.dispose()
|
334 | ```
|
335 |
|
336 | #### Asyncify
|
337 |
|
338 | Sometimes, we want to create a function that's synchronous from the perspective
|
339 | of QuickJS, but prefer to implement that function _asynchronously_ in your host
|
340 | code. The most obvious use-case is for EcmaScript module loading. The underlying
|
341 | QuickJS C library expects the module loader function to return synchronously,
|
342 | but loading data synchronously in the browser or server is somewhere between "a
|
343 | bad idea" and "impossible". QuickJS also doesn't expose an API to "pause" the
|
344 | execution of a runtime, and adding such an API is tricky due to the VM's
|
345 | implementation.
|
346 |
|
347 | As a work-around, we provide an alternate build of QuickJS processed by
|
348 | Emscripten/Binaryen's [ASYNCIFY](https://emscripten.org/docs/porting/asyncify.html)
|
349 | compiler transform. Here's how Emscripten's documentation describes Asyncify:
|
350 |
|
351 | > Asyncify lets synchronous C or C++ code interact with asynchronous \[host] JavaScript. This allows things like:
|
352 | >
|
353 | > - A synchronous call in C that yields to the event loop, which allows browser events to be handled.
|
354 | > - A synchronous call in C that waits for an asynchronous operation in \[host] JS to complete.
|
355 | >
|
356 | > Asyncify automatically transforms ... code into a form that can be paused and
|
357 | > resumed ..., so that it is asynchronous (hence the name “Asyncify”) even though
|
358 | > \[it is written] in a normal synchronous way.
|
359 |
|
360 | This means we can suspend an _entire WebAssembly module_ (which could contain
|
361 | multiple runtimes and contexts) while our host Javascript loads data
|
362 | asynchronously, and then resume execution once the data load completes. This is
|
363 | a very handy superpower, but it comes with a couple of major limitations:
|
364 |
|
365 | 1. _An asyncified WebAssembly module can only suspend to wait for a single
|
366 | asynchronous call at a time_. You may call back into a suspended WebAssembly
|
367 | module eg. to create a QuickJS value to return a result, but the system will
|
368 | crash if this call tries to suspend again. Take a look at Emscripten's documentation
|
369 | on [reentrancy](https://emscripten.org/docs/porting/asyncify.html#reentrancy).
|
370 |
|
371 | 2. _Asyncified code is bigger and runs slower_. The asyncified build of
|
372 | Quickjs-emscripten library is 1M, 2x larger than the 500K of the default
|
373 | version. There may be room for further
|
374 | [optimization](https://emscripten.org/docs/porting/asyncify.html#optimizing)
|
375 | Of our build in the future.
|
376 |
|
377 | To use asyncify features, use the following functions:
|
378 |
|
379 | - [newAsyncRuntime][]: create a runtime inside a new WebAssembly module.
|
380 | - [newAsyncContext][]: create runtime and context together inside a new
|
381 | WebAssembly module.
|
382 | - [newQuickJSAsyncWASMModule][]: create an empty WebAssembly module.
|
383 |
|
384 | [newasyncruntime]: https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten/exports.md#newasyncruntime
|
385 | [newasynccontext]: https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten/exports.md#newasynccontext
|
386 | [newquickjsasyncwasmmodule]: https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten/exports.md#newquickjsasyncwasmmodule
|
387 |
|
388 | These functions are asynchronous because they always create a new underlying
|
389 | WebAssembly module so that each instance can suspend and resume independently,
|
390 | and instantiating a WebAssembly module is an async operation. This also adds
|
391 | substantial overhead compared to creating a runtime or context inside an
|
392 | existing module; if you only need to wait for a single async action at a time,
|
393 | you can create a single top-level module and create runtimes or contexts inside
|
394 | of it.
|
395 |
|
396 | ##### Async module loader
|
397 |
|
398 | Here's an example of valuating a script that loads React asynchronously as an ES
|
399 | module. In our example, we're loading from the filesystem for reproducibility,
|
400 | but you can use this technique to load using `fetch`.
|
401 |
|
402 | ```typescript
|
403 | const module = await newQuickJSAsyncWASMModule()
|
404 | const runtime = module.newRuntime()
|
405 | const path = await import("path")
|
406 | const { promises: fs } = await import("fs")
|
407 |
|
408 | const importsPath = path.join(__dirname, "../examples/imports") + "/"
|
409 | // Module loaders can return promises.
|
410 | // Execution will suspend until the promise resolves.
|
411 | runtime.setModuleLoader((moduleName) => {
|
412 | const modulePath = path.join(importsPath, moduleName)
|
413 | if (!modulePath.startsWith(importsPath)) {
|
414 | throw new Error("out of bounds")
|
415 | }
|
416 | console.log("loading", moduleName, "from", modulePath)
|
417 | return fs.readFile(modulePath, "utf-8")
|
418 | })
|
419 |
|
420 | // evalCodeAsync is required when execution may suspend.
|
421 | const context = runtime.newContext()
|
422 | const result = await context.evalCodeAsync(`
|
423 | import * as React from 'esm.sh/react@17'
|
424 | import * as ReactDOMServer from 'esm.sh/react-dom@17/server'
|
425 | const e = React.createElement
|
426 | globalThis.html = ReactDOMServer.renderToStaticMarkup(
|
427 | e('div', null, e('strong', null, 'Hello world!'))
|
428 | )
|
429 | `)
|
430 | context.unwrapResult(result).dispose()
|
431 | const html = context.getProp(context.global, "html").consume(context.getString)
|
432 | console.log(html) // <div><strong>Hello world!</strong></div>
|
433 | ```
|
434 |
|
435 | ##### Async on host, sync in QuickJS
|
436 |
|
437 | Here's an example of turning an async function into a sync function inside the
|
438 | VM.
|
439 |
|
440 | ```typescript
|
441 | const context = await newAsyncContext()
|
442 | const path = await import("path")
|
443 | const { promises: fs } = await import("fs")
|
444 |
|
445 | const importsPath = path.join(__dirname, "../examples/imports") + "/"
|
446 | const readFileHandle = context.newAsyncifiedFunction("readFile", async (pathHandle) => {
|
447 | const pathString = path.join(importsPath, context.getString(pathHandle))
|
448 | if (!pathString.startsWith(importsPath)) {
|
449 | throw new Error("out of bounds")
|
450 | }
|
451 | const data = await fs.readFile(pathString, "utf-8")
|
452 | return context.newString(data)
|
453 | })
|
454 | readFileHandle.consume((fn) => context.setProp(context.global, "readFile", fn))
|
455 |
|
456 | // evalCodeAsync is required when execution may suspend.
|
457 | const result = await context.evalCodeAsync(`
|
458 | // Not a promise! Sync! vvvvvvvvvvvvvvvvvvvv
|
459 | const data = JSON.parse(readFile('data.json'))
|
460 | data.map(x => x.toUpperCase()).join(' ')
|
461 | `)
|
462 | const upperCaseData = context.unwrapResult(result).consume(context.getString)
|
463 | console.log(upperCaseData) // 'VERY USEFUL DATA'
|
464 | ```
|
465 |
|
466 | ### Testing your code
|
467 |
|
468 | This library is complicated to use, so please consider automated testing your
|
469 | implementation. We highly writing your test suite to run with both the "release"
|
470 | build variant of quickjs-emscripten, and also the [DEBUG_SYNC] build variant.
|
471 | The debug sync build variant has extra instrumentation code for detecting memory
|
472 | leaks.
|
473 |
|
474 | The class [TestQuickJSWASMModule] exposes the memory leak detection API, although
|
475 | this API is only accurate when using `DEBUG_SYNC` variant.
|
476 |
|
477 | ```typescript
|
478 | // Define your test suite in a function, so that you can test against
|
479 | // different module loaders.
|
480 | function myTests(moduleLoader: () => Promise<QuickJSWASMModule>) {
|
481 | let QuickJS: TestQuickJSWASMModule
|
482 | beforeEach(async () => {
|
483 | // Get a unique TestQuickJSWASMModule instance for each test.
|
484 | const wasmModule = await moduleLoader()
|
485 | QuickJS = new TestQuickJSWASMModule(wasmModule)
|
486 | })
|
487 | afterEach(() => {
|
488 | // Assert that the test disposed all handles. The DEBUG_SYNC build
|
489 | // variant will show detailed traces for each leak.
|
490 | QuickJS.assertNoMemoryAllocated()
|
491 | })
|
492 |
|
493 | it("works well", () => {
|
494 | // TODO: write a test using QuickJS
|
495 | const context = QuickJS.newContext()
|
496 | context.unwrapResult(context.evalCode("1 + 1")).dispose()
|
497 | context.dispose()
|
498 | })
|
499 | }
|
500 |
|
501 | // Run the test suite against a matrix of module loaders.
|
502 | describe("Check for memory leaks with QuickJS DEBUG build", () => {
|
503 | const moduleLoader = memoizePromiseFactory(() => newQuickJSWASMModule(DEBUG_SYNC))
|
504 | myTests(moduleLoader)
|
505 | })
|
506 |
|
507 | describe("Realistic test with QuickJS RELEASE build", () => {
|
508 | myTests(getQuickJS)
|
509 | })
|
510 | ```
|
511 |
|
512 | For more testing examples, please explore the typescript source of [quickjs-emscripten][ts] repository.
|
513 |
|
514 | [ts]: https://github.com/justjake/quickjs-emscripten/blob/main/packages/quickjs-emscripten/src/
|
515 | [debug_sync]: https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten/exports.md#debug_sync
|
516 | [testquickjswasmmodule]: https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten/classes/TestQuickJSWASMModule.md
|
517 |
|
518 | ### Using in the browser without a build step
|
519 |
|
520 | You can use quickjs-emscripten directly from an HTML file in two ways:
|
521 |
|
522 | 1. Import it in an ES Module script tag
|
523 |
|
524 | ```html
|
525 | <!doctype html>
|
526 | <!-- Import from a ES Module CDN -->
|
527 | <script type="module">
|
528 | import { getQuickJS } from "https://esm.sh/quickjs-emscripten@0.25.0"
|
529 | const QuickJS = await getQuickJS()
|
530 | console.log(QuickJS.evalCode("1+1"))
|
531 | </script>
|
532 | ```
|
533 |
|
534 | 1. In edge cases, you might want to use the IIFE build which provides QuickJS as the global `QJS`. You should probably use the ES module though, any recent browser supports it.
|
535 |
|
536 | ```html
|
537 | <!doctype html>
|
538 | <!-- Add a script tag to load the library as the QJS global -->
|
539 | <script
|
540 | src="https://cdn.jsdelivr.net/npm/quickjs-emscripten@0.25.0/dist/index.global.js"
|
541 | type="text/javascript"
|
542 | ></script>
|
543 | <!-- Then use the QJS global in a script tag -->
|
544 | <script type="text/javascript">
|
545 | QJS.getQuickJS().then((QuickJS) => {
|
546 | console.log(QuickJS.evalCode("1+1"))
|
547 | })
|
548 | </script>
|
549 | ```
|
550 |
|
551 | ### quickjs-emscripten-core, variants, and advanced packaging
|
552 |
|
553 | Them main `quickjs-emscripten` package includes several build variants of the WebAssembly module.
|
554 | If these variants are too large for you, you can instead use the `quickjs-emscripten-core` package,
|
555 | and manually select your own build variant.
|
556 |
|
557 | See the [documentation of quickjs-emscripten-core][core] for more details.
|
558 |
|
559 | [core]: https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten-core/README.md
|
560 |
|
561 | ### Debugging
|
562 |
|
563 | - Switch to a DEBUG build variant of the WebAssembly module to see debug log messages from the C part of this library:
|
564 |
|
565 | ```typescript
|
566 | import { newQuickJSWASMModule, DEBUG_SYNC } from "quickjs-emscripten"
|
567 |
|
568 | const QuickJS = await newQuickJSWASMModule(DEBUG_SYNC)
|
569 | ```
|
570 |
|
571 | With quickjs-emscripten-core:
|
572 |
|
573 | ```typescript
|
574 | import { newQuickJSWASMModuleFromVariant } from "quickjs-emscripten-core"
|
575 | import DEBUG_SYNC from "@jitl/quickjs-wasmfile-debug-sync"
|
576 |
|
577 | const QuickJS = await newQuickJSWASMModuleFromVariant(DEBUG_SYNC)
|
578 | ```
|
579 |
|
580 | - Enable debug log messages from the Javascript part of this library with [setDebugMode][setDebugMode]:
|
581 |
|
582 | ```typescript
|
583 | import { setDebugMode } from "quickjs-emscripten"
|
584 |
|
585 | setDebugMode(true)
|
586 | ```
|
587 |
|
588 | With quickjs-emscripten-core:
|
589 |
|
590 | ```typescript
|
591 | import { setDebugMode } from "quickjs-emscripten-core"
|
592 |
|
593 | setDebugMode(true)
|
594 | ```
|
595 |
|
596 | [setDebugMode]: doc/quickjs-emscripten/exports.md#setdebugmode
|
597 |
|
598 | ### More Documentation
|
599 |
|
600 | [Github] | [NPM] | [API Documentation][api] | [Variants][core] | [Examples][tests]
|
601 |
|
602 | ### Requirements
|
603 |
|
604 | `quickjs-emscripten` and related packages should work in any environment that supports ES2020.
|
605 |
|
606 | - NodeJS: requires v16.0.0 or later for WebAssembly compatibility. Tested with node@18.
|
607 | - We estimate support for the following browsers:
|
608 | - Chrome 63+
|
609 | - Edge 79+
|
610 | - Safari 11.1+
|
611 | - Firefox 58+
|
612 | - Webpack: tested with webpack@5.89.0 via create-react-app.
|
613 | - Vite: tested with vite@5.0.10.
|
614 | - Typescript: tested with typescript@4.5.5 and typescript@5.3.3.
|
615 | - Create react app: tested with react-scripts@5.0.1.
|
616 |
|
617 | ## Background
|
618 |
|
619 | This was inspired by seeing https://github.com/maple3142/duktape-eval
|
620 | [on Hacker News](https://news.ycombinator.com/item?id=21946565) and Figma's
|
621 | blogposts about using building a Javascript plugin runtime:
|
622 |
|
623 | - [How Figma built the Figma plugin system](https://www.figma.com/blog/how-we-built-the-figma-plugin-system/): Describes the LowLevelJavascriptVm interface.
|
624 | - [An update on plugin security](https://www.figma.com/blog/an-update-on-plugin-security/): Figma switches to QuickJS.
|
625 |
|
626 | ## Status & Roadmap
|
627 |
|
628 | **Stability**: Because the version number of this project is below `1.0.0`,
|
629 | \*expect occasional breaking API changes.
|
630 |
|
631 | **Security**: This project makes every effort to be secure, but has not been
|
632 | audited. Please use with care in production settings.
|
633 |
|
634 | **Roadmap**: I work on this project in my free time, for fun. Here's I'm
|
635 | thinking comes next. Last updated 2022-03-18.
|
636 |
|
637 | 1. Further work on module loading APIs:
|
638 |
|
639 | - Create modules via Javascript, instead of source text.
|
640 | - Scan source text for imports, for ahead of time or concurrent loading.
|
641 | (This is possible with third-party tools, so lower priority.)
|
642 |
|
643 | 2. Higher-level tools for reading QuickJS values:
|
644 |
|
645 | - Type guard functions: `context.isArray(handle)`, `context.isPromise(handle)`, etc.
|
646 | - Iteration utilities: `context.getIterable(handle)`, `context.iterateObjectEntries(handle)`.
|
647 | This better supports user-level code to deserialize complex handle objects.
|
648 |
|
649 | 3. Higher-level tools for creating QuickJS values:
|
650 |
|
651 | - Devise a way to avoid needing to mess around with handles when setting up
|
652 | the environment.
|
653 | - Consider integrating
|
654 | [quickjs-emscripten-sync](https://github.com/reearth/quickjs-emscripten-sync)
|
655 | for automatic translation.
|
656 | - Consider class-based or interface-type-based marshalling.
|
657 |
|
658 | 4. SQLite integration.
|
659 |
|
660 | ## Related
|
661 |
|
662 | - Duktape wrapped in Wasm: https://github.com/maple3142/duktape-eval/blob/main/src/Makefile
|
663 | - QuickJS wrapped in C++: https://github.com/ftk/quickjspp
|
664 |
|
665 | ## Developing
|
666 |
|
667 | This library is implemented in two languages: C (compiled to WASM with
|
668 | Emscripten), and Typescript.
|
669 |
|
670 | You will need `node`, `yarn`, `make`, and `emscripten` to build this project.
|
671 |
|
672 | ### The C parts
|
673 |
|
674 | The ./c directory contains C code that wraps the QuickJS C library (in ./quickjs).
|
675 | Public functions (those starting with `QTS_`) in ./c/interface.c are
|
676 | automatically exported to native code (via a generated header) and to
|
677 | Typescript (via a generated FFI class). See ./generate.ts for how this works.
|
678 |
|
679 | The C code builds with `emscripten` (using `emcc`), to produce WebAssembly.
|
680 | The version of Emscripten used by the project is defined in templates/Variant.mk.
|
681 |
|
682 | - On ARM64, you should install `emscripten` on your machine. For example on macOS, `brew install emscripten`.
|
683 | - If _the correct version of emcc_ is not in your PATH, compilation falls back to using Docker.
|
684 | On ARM64, this is 10-50x slower than native compilation, but it's just fine on x64.
|
685 |
|
686 | We produce multiple build variants of the C code compiled to WebAssembly using a
|
687 | template script the ./packages directory. Each build variant uses its own copy of a Makefile
|
688 | to build the C code. The Makefile is generated from a template in ./templates/Variant.mk.
|
689 |
|
690 | Related NPM scripts:
|
691 |
|
692 | - `yarn update-quickjs` will sync the ./quickjs folder with a github repo tracking the upstream QuickJS.
|
693 | - `yarn build:codegen` updates the ./packages from the template script `./prepareVariants.ts` and Variant.mk.
|
694 | - `yarn build:packages` builds the variant packages in parallel.
|
695 |
|
696 | ### The Typescript parts
|
697 |
|
698 | The Javascript/Typescript code is also organized into several NPM packages in ./packages:
|
699 |
|
700 | - ./packages/quickjs-ffi-types: Low-level types that define the FFI interface to the C code.
|
701 | Each variant exposes an API conforming to these types that's consumed by the higher-level library.
|
702 | - ./packages/quickjs-emscripten-core: The higher-level Typescript that implements the user-facing abstractions of the library.
|
703 | This package doesn't link directly to the WebAssembly/C code; callers must provide a build variant.
|
704 | - ./packages/quicks-emscripten: The main entrypoint of the library, which provides the `getQuickJS` function.
|
705 | This package combines quickjs-emscripten-core with platform-appropriate WebAssembly/C code.
|
706 |
|
707 | Related NPM scripts:
|
708 |
|
709 | - `yarn check` runs all available checks (build, format, tests, etc).
|
710 | - `yarn build` builds all the packages and generates the docs.
|
711 | - `yarn test` runs the tests for all packages.
|
712 | - `yarn test:fast` runs the tests using only fast build variants.
|
713 | - `yarn doc` generates the docs into `./doc`.
|
714 | - `yarn doc:serve` previews the current `./doc` in a browser.
|
715 | - `yarn prettier` formats the repo.
|
716 |
|
717 | ### Yarn updates
|
718 |
|
719 | Just run `yarn set version from sources` to upgrade the Yarn release.
|
720 |
|
\ | No newline at end of file |