UNPKG

28.5 kBMarkdownView Raw
1# quickjs-emscripten
2
3Javascript/Typescript bindings for QuickJS, a modern Javascript interpreter,
4compiled 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
14import { getQuickJS } from "quickjs-emscripten"
15
16async 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
36main()
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
76Install from `npm`: `npm install --save quickjs-emscripten` or `yarn add quickjs-emscripten`.
77
78The root entrypoint of this library is the `getQuickJS` function, which returns
79a promise that resolves to a [QuickJSWASMModule](./doc/quickjs-emscripten/classes/QuickJSWASMModule.md) when
80the QuickJS WASM module is ready.
81
82Once `getQuickJS` has been awaited at least once, you also can use the `getQuickJSSync`
83function to directly access the singleton in your synchronous code.
84
85### Safely evaluate Javascript code
86
87See [QuickJSWASMModule.evalCode](https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten/classes/QuickJSWASMModule.md#evalcode)
88
89```typescript
90import { getQuickJS, shouldInterruptAfterDeadline } from "quickjs-emscripten"
91
92getQuickJS().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
103You can use [QuickJSContext](https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten/classes/QuickJSContext.md)
104to build a scripting environment by modifying globals and exposing functions
105into the QuickJS interpreter.
106
107Each `QuickJSContext` instance has its own environment -- globals, built-in
108classes -- and actions from one context won't leak into other contexts or
109runtimes (with one exception, see [Asyncify][asyncify]).
110
111Every context is created inside a
112[QuickJSRuntime](https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten/classes/QuickJSRuntime.md).
113A runtime represents a Javascript heap, and you can even share values between
114contexts in the same runtime.
115
116```typescript
117const vm = QuickJS.newContext()
118let state = 0
119
120const fnHandle = vm.newFunction("nextId", () => {
121 return vm.newNumber(++state)
122})
123
124vm.setProp(vm.global, "nextId", fnHandle)
125fnHandle.dispose()
126
127const nextId = vm.unwrapResult(vm.evalCode(`nextId(); nextId(); nextId()`))
128console.log("vm result:", vm.getNumber(nextId), "native state:", state)
129
130nextId.dispose()
131vm.dispose()
132```
133
134When you create a context from a top-level API like in the example above,
135instead of by calling `runtime.newContext()`, a runtime is automatically created
136for the lifetime of the context, and disposed of when you dispose the context.
137
138#### Runtime
139
140The runtime has APIs for CPU and memory limits that apply to all contexts within
141the runtime in aggregate. You can also use the runtime to configure EcmaScript
142module loading.
143
144```typescript
145const runtime = QuickJS.newRuntime()
146// "Should be enough for everyone" -- attributed to B. Gates
147runtime.setMemoryLimit(1024 * 640)
148// Limit stack size
149runtime.setMaxStackSize(1024 * 320)
150// Interrupt computation after 1024 calls to the interrupt handler
151let interruptCycles = 0
152runtime.setInterruptHandler(() => ++interruptCycles > 1024)
153// Toy module system that always returns the module name
154// as the default export
155runtime.setModuleLoader((moduleName) => `export default '${moduleName}'`)
156const context = runtime.newContext()
157const ok = context.evalCode(`
158import fooName from './foo.js'
159globalThis.result = fooName
160`)
161context.unwrapResult(ok).dispose()
162// logs "foo.js"
163console.log(context.getProp(context.global, "result").consume(context.dump))
164context.dispose()
165runtime.dispose()
166```
167
168### Memory Management
169
170Many methods in this library return handles to memory allocated inside the
171WebAssembly heap. These types cannot be garbage-collected as usual in
172Javascript. Instead, you must manually manage their memory by calling a
173`.dispose()` method to free the underlying resources. Once a handle has been
174disposed, 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
177Calling `QuickJSContext.dispose()` will throw a RuntimeError if you've forgotten to
178dispose any handles associated with that VM, so it's good practice to create a
179new VM instance for each of your tests, and to call `vm.dispose()` at the end
180of every test.
181
182```typescript
183const vm = QuickJS.newContext()
184const numberHandle = vm.newNumber(42)
185// Note: numberHandle not disposed, so it leaks memory.
186vm.dispose()
187// throws RuntimeError: abort(Assertion failed: list_empty(&rt->gc_obj_list), at: quickjs/quickjs.c,1963,JS_FreeRuntime)
188```
189
190Here are some strategies to reduce the toil of calling `.dispose()` on each
191handle you create:
192
193#### Scope
194
195A
196[`Scope`](https://github.com/justjake/quickjs-emscripten/blob/main/doc/quickjs-emscripten/classes/Scope.md#class-scope)
197instance manages a set of disposables and calls their `.dispose()`
198method 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
202Scope.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
222You can also create `Scope` instances with `new Scope()` if you want to manage
223calling `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)
228is sugar for the common pattern of using a handle and then
229immediately disposing of it. `Lifetime.consume` takes a `map` function that
230produces a result of any type. The `map` fuction is called with the handle,
231then the handle is disposed, then the result is returned.
232
233Here's the "Interfacing with interpreter" example re-written using `.consume()`:
234
235```typescript
236const vm = QuickJS.newContext()
237let state = 0
238
239vm.newFunction("nextId", () => {
240 return vm.newNumber(++state)
241}).consume((fnHandle) => vm.setProp(vm.global, "nextId", fnHandle))
242
243vm.unwrapResult(vm.evalCode(`nextId(); nextId(); nextId()`)).consume((nextId) =>
244 console.log("vm result:", vm.getNumber(nextId), "native state:", state),
245)
246
247vm.dispose()
248```
249
250Generally 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
255To add APIs inside the QuickJS environment, you'll need to create objects to
256define the shape of your API, and add properties and functions to those objects
257to allow code inside QuickJS to call code on the host.
258
259By default, no host functionality is exposed to code running inside QuickJS.
260
261```typescript
262const vm = QuickJS.newContext()
263// `console.log`
264const logHandle = vm.newFunction("log", (...args) => {
265 const nativeArgs = args.map(vm.dump)
266 console.log("QuickJS:", ...nativeArgs)
267})
268// Partially implement `console` object
269const consoleHandle = vm.newObject()
270vm.setProp(consoleHandle, "log", logHandle)
271vm.setProp(vm.global, "console", consoleHandle)
272consoleHandle.dispose()
273logHandle.dispose()
274
275vm.unwrapResult(vm.evalCode(`console.log("Hello from QuickJS!")`)).dispose()
276```
277
278#### Promises
279
280To expose an asynchronous function that _returns a promise_ to callers within
281QuickJS, your function can return the handle of a `QuickJSDeferredPromise`
282created via `context.newPromise()`.
283
284When you resolve a `QuickJSDeferredPromise` -- and generally whenever async
285behavior completes for the VM -- pending listeners inside QuickJS may not
286execute immediately. Your code needs to explicitly call
287`runtime.executePendingJobs()` to resume execution inside QuickJS. This API
288gives your code maximum control to _schedule_ when QuickJS will block the host's
289event loop by resuming execution.
290
291To work with QuickJS handles that contain a promise inside the environment, you
292can convert the QuickJSHandle into a native promise using
293`context.resolvePromise()`. Take care with this API to avoid 'deadlocks' where
294the host awaits a guest promise, but the guest cannot make progress until the
295host calls `runtime.executePendingJobs()`. The simplest way to avoid this kind
296of deadlock is to always schedule `executePendingJobs` after any promise is
297settled.
298
299```typescript
300const vm = QuickJS.newContext()
301const fakeFileSystem = new Map([["example.txt", "Example file content"]])
302
303// Function that simulates reading data asynchronously
304const 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})
317readFileHandle.consume((handle) => vm.setProp(vm.global, "readFile", handle))
318
319// Evaluate code that uses `readFile`, which returns a promise
320const result = vm.evalCode(`(async () => {
321 const content = await readFile('example.txt')
322 return content.toUpperCase()
323})()`)
324const 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.
329const resolvedResult = await vm.resolvePromise(promiseHandle)
330promiseHandle.dispose()
331const resolvedHandle = vm.unwrapResult(resolvedResult)
332console.log("Result:", vm.getString(resolvedHandle))
333resolvedHandle.dispose()
334```
335
336#### Asyncify
337
338Sometimes, we want to create a function that's synchronous from the perspective
339of QuickJS, but prefer to implement that function _asynchronously_ in your host
340code. The most obvious use-case is for EcmaScript module loading. The underlying
341QuickJS C library expects the module loader function to return synchronously,
342but loading data synchronously in the browser or server is somewhere between "a
343bad idea" and "impossible". QuickJS also doesn't expose an API to "pause" the
344execution of a runtime, and adding such an API is tricky due to the VM's
345implementation.
346
347As a work-around, we provide an alternate build of QuickJS processed by
348Emscripten/Binaryen's [ASYNCIFY](https://emscripten.org/docs/porting/asyncify.html)
349compiler 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
360This means we can suspend an _entire WebAssembly module_ (which could contain
361multiple runtimes and contexts) while our host Javascript loads data
362asynchronously, and then resume execution once the data load completes. This is
363a very handy superpower, but it comes with a couple of major limitations:
364
3651. _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
3712. _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
377To 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
388These functions are asynchronous because they always create a new underlying
389WebAssembly module so that each instance can suspend and resume independently,
390and instantiating a WebAssembly module is an async operation. This also adds
391substantial overhead compared to creating a runtime or context inside an
392existing module; if you only need to wait for a single async action at a time,
393you can create a single top-level module and create runtimes or contexts inside
394of it.
395
396##### Async module loader
397
398Here's an example of valuating a script that loads React asynchronously as an ES
399module. In our example, we're loading from the filesystem for reproducibility,
400but you can use this technique to load using `fetch`.
401
402```typescript
403const module = await newQuickJSAsyncWASMModule()
404const runtime = module.newRuntime()
405const path = await import("path")
406const { promises: fs } = await import("fs")
407
408const importsPath = path.join(__dirname, "../examples/imports") + "/"
409// Module loaders can return promises.
410// Execution will suspend until the promise resolves.
411runtime.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.
421const context = runtime.newContext()
422const result = await context.evalCodeAsync(`
423import * as React from 'esm.sh/react@17'
424import * as ReactDOMServer from 'esm.sh/react-dom@17/server'
425const e = React.createElement
426globalThis.html = ReactDOMServer.renderToStaticMarkup(
427 e('div', null, e('strong', null, 'Hello world!'))
428)
429`)
430context.unwrapResult(result).dispose()
431const html = context.getProp(context.global, "html").consume(context.getString)
432console.log(html) // <div><strong>Hello world!</strong></div>
433```
434
435##### Async on host, sync in QuickJS
436
437Here's an example of turning an async function into a sync function inside the
438VM.
439
440```typescript
441const context = await newAsyncContext()
442const path = await import("path")
443const { promises: fs } = await import("fs")
444
445const importsPath = path.join(__dirname, "../examples/imports") + "/"
446const 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})
454readFileHandle.consume((fn) => context.setProp(context.global, "readFile", fn))
455
456// evalCodeAsync is required when execution may suspend.
457const result = await context.evalCodeAsync(`
458// Not a promise! Sync! vvvvvvvvvvvvvvvvvvvv
459const data = JSON.parse(readFile('data.json'))
460data.map(x => x.toUpperCase()).join(' ')
461`)
462const upperCaseData = context.unwrapResult(result).consume(context.getString)
463console.log(upperCaseData) // 'VERY USEFUL DATA'
464```
465
466### Testing your code
467
468This library is complicated to use, so please consider automated testing your
469implementation. We highly writing your test suite to run with both the "release"
470build variant of quickjs-emscripten, and also the [DEBUG_SYNC] build variant.
471The debug sync build variant has extra instrumentation code for detecting memory
472leaks.
473
474The class [TestQuickJSWASMModule] exposes the memory leak detection API, although
475this 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.
480function 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.
502describe("Check for memory leaks with QuickJS DEBUG build", () => {
503 const moduleLoader = memoizePromiseFactory(() => newQuickJSWASMModule(DEBUG_SYNC))
504 myTests(moduleLoader)
505})
506
507describe("Realistic test with QuickJS RELEASE build", () => {
508 myTests(getQuickJS)
509})
510```
511
512For 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
520You can use quickjs-emscripten directly from an HTML file in two ways:
521
5221. 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
5341. 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
553Them main `quickjs-emscripten` package includes several build variants of the WebAssembly module.
554If these variants are too large for you, you can instead use the `quickjs-emscripten-core` package,
555and manually select your own build variant.
556
557See 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
619This was inspired by seeing https://github.com/maple3142/duktape-eval
620[on Hacker News](https://news.ycombinator.com/item?id=21946565) and Figma's
621blogposts 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
632audited. 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
635thinking comes next. Last updated 2022-03-18.
636
6371. 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
6432. 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
6493. 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
6584. 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
667This library is implemented in two languages: C (compiled to WASM with
668Emscripten), and Typescript.
669
670You will need `node`, `yarn`, `make`, and `emscripten` to build this project.
671
672### The C parts
673
674The ./c directory contains C code that wraps the QuickJS C library (in ./quickjs).
675Public functions (those starting with `QTS_`) in ./c/interface.c are
676automatically exported to native code (via a generated header) and to
677Typescript (via a generated FFI class). See ./generate.ts for how this works.
678
679The C code builds with `emscripten` (using `emcc`), to produce WebAssembly.
680The 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
686We produce multiple build variants of the C code compiled to WebAssembly using a
687template script the ./packages directory. Each build variant uses its own copy of a Makefile
688to build the C code. The Makefile is generated from a template in ./templates/Variant.mk.
689
690Related 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
698The 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
707Related 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
719Just run `yarn set version from sources` to upgrade the Yarn release.
720
\No newline at end of file