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