UNPKG

9.86 kBMarkdownView Raw
1# quickjs-emscripten
2
3Javascript/Typescript bindings for [QuickJS, a modern Javascript interpreter written in
4C by Fabrice Bellard](https://bellard.org/quickjs/) compiled to WebAssembly.
5
6- Safely evaluate untrusted Javascript (up to ES2020).
7- Create and manipulate values inside the QuickJS runtime.
8- Expose host functions to the QuickJS runtime.
9
10```typescript
11import { getQuickJS } from 'quickjs-emscripten'
12
13async function main() {
14 const QuickJS = await getQuickJS()
15 const vm = QuickJS.createVm()
16
17 const world = vm.newString('world')
18 vm.setProp(vm.global, 'NAME', world)
19 world.dispose()
20
21 const result = vm.evalCode(`"Hello " + NAME + "!"`)
22 if (result.error) {
23 console.log('Execution failed:', vm.dump(result.error))
24 result.error.dispose()
25 } else {
26 console.log('Success:', vm.dump(result.value))
27 result.value.dispose()
28 }
29
30 vm.dispose()
31}
32
33main()
34```
35
36## Usage
37
38Install from `npm`: `npm install --save quickjs-emscripten` or `yarn add quickjs-emscripten`.
39
40The root entrypoint of this library is the `getQuickJS` function, which returns
41a promise that resolves to a [QuickJS singleton](doc/classes/quickjs.md) when
42the Emscripten WASM module is ready.
43
44Once `getQuickJS` has been awaited at least once, you also can use the `getQuickJSSync`
45function to directly access the singleton engine in your synchronous code.
46
47### Safely evaluate Javascript code
48
49See [QuickJS.evalCode](https://github.com/justjake/quickjs-emscripten/blob/master/doc/classes/quickjs.md#evalcode)
50
51```typescript
52import { getQuickJS, shouldInterruptAfterDeadline } from 'quickjs-emscripten'
53
54getQuickJS().then(QuickJS => {
55 const result = QuickJS.evalCode('1 + 1', {
56 shouldInterrupt: shouldInterruptAfterDeadline(Date.now() + 1000),
57 memoryLimitBytes: 1024 * 1024,
58 })
59 console.log(result)
60})
61```
62
63### Interfacing with the interpreter
64
65You can use [QuickJSVm](https://github.com/justjake/quickjs-emscripten/blob/master/doc/classes/quickjsvm.md)
66to build a scripting environment by modifying globals and exposing functions
67into the QuickJS interpreter.
68
69Each `QuickJSVm` instance has its own environment, CPU limit, and memory
70limit. See the documentation for details.
71
72```typescript
73const vm = QuickJS.createVm()
74let state = 0
75
76const fnHandle = vm.newFunction('nextId', () => {
77 return vm.newNumber(++state)
78})
79
80vm.setProp(vm.global, 'nextId', fnHandle)
81fnHandle.dispose()
82
83const nextId = vm.unwrapResult(vm.evalCode(`nextId(); nextId(); nextId()`))
84console.log('vm result:', vm.getNumber(nextId), 'native state:', state)
85
86nextId.dispose()
87vm.dispose()
88```
89
90### Memory Management
91
92Many methods in this library return handles to memory allocated inside the
93WebAssembly heap. These types cannot be garbage-collected as usual in
94Javascript. Instead, you must manually manage their memory by calling a
95`.dispose()` method to free the underlying resources. Once a handle has been
96disposed, it cannot be used anymore. Note that in the example above, we call
97`.dispose()` on each handle once it is no longer needed.
98
99Calling `QuickJSVm.dispose()` will throw a RuntimeError if you've forgotten to
100dispose any handles associated with that VM, so it's good practice to create a
101new VM instance for each of your tests, and to call `vm.dispose()` at the end
102of every test.
103
104```typescript
105const vm = QuickJS.createVm()
106const numberHandle = vm.newNumber(42)
107// Note: numberHandle not disposed, so it leaks memory.
108vm.dispose()
109// throws RuntimeError: abort(Assertion failed: list_empty(&rt->gc_obj_list), at: quickjs/quickjs.c,1963,JS_FreeRuntime)
110```
111
112Here are some strategies to reduce the toil of calling `.dispose()` on each
113handle you create:
114
115#### Scope
116
117A
118[`Scope`](https://github.com/justjake/quickjs-emscripten/blob/master/doc/classes/scope.md#class-scope)
119instance manages a set of disposables and calls their `.dispose()`
120method in the reverse order in which they're added to the scope. Here's the
121"Interfacing with the interpreter" example re-written using `Scope`:
122
123```typescript
124Scope.withScope(scope => {
125 const vm = scope.manage(QuickJS.createVm())
126 let state = 0
127
128 const fnHandle = scope.manage(
129 vm.newFunction('nextId', () => {
130 return vm.newNumber(++state)
131 })
132 )
133
134 vm.setProp(vm.global, 'nextId', fnHandle)
135
136 const nextId = scope.manage(vm.unwrapResult(vm.evalCode(`nextId(); nextId(); nextId()`)))
137 console.log('vm result:', vm.getNumber(nextId), 'native state:', state)
138
139 // When the withScope block exits, it calls scope.dispose(), which in turn calls
140 // the .dispose() methods of all the disposables managed by the scope.
141})
142```
143
144You can also create `Scope` instances with `new Scope()` if you want to manage
145calling `scope.dispose()` yourself.
146
147#### `Lifetime.consume(fn)`
148
149[`Lifetime.consume`](https://github.com/justjake/quickjs-emscripten/blob/master/doc/classes/lifetime.md#consume)
150is sugar for the common pattern of using a handle and then
151immediately disposing of it. `Lifetime.consume` takes a `map` function that
152produces a result of any type. The `map` fuction is called with the handle,
153then the handle is disposed, then the result is returned.
154
155Here's the "Interfacing with interpreter" example re-written using `.consume()`:
156
157```typescript
158const vm = QuickJS.createVm()
159let state = 0
160
161vm.newFunction('nextId', () => {
162 return vm.newNumber(++state)
163}).consume(fnHandle => vm.setProp(vm.global, 'nextId', fnHandle))
164
165vm.unwrapResult(vm.evalCode(`nextId(); nextId(); nextId()`)).consume(nextId =>
166 console.log('vm result:', vm.getNumber(nextId), 'native state:', state)
167)
168
169vm.dispose()
170```
171
172Generally working with `Scope` leads to more straight-forward code, but
173`Lifetime.consume` can be handy sugar as part of a method call chain.
174
175### More Documentation
176
177- [API Documentation](https://github.com/justjake/quickjs-emscripten/blob/master/doc/globals.md)
178- [Examples](https://github.com/justjake/quickjs-emscripten/blob/master/ts/quickjs.test.ts)
179
180## Background
181
182This was inspired by seeing https://github.com/maple3142/duktape-eval
183[on Hacker News](https://news.ycombinator.com/item?id=21946565) and Figma's
184blogposts about using building a Javascript plugin runtime:
185
186- [How Figma built the Figma plugin system](https://www.figma.com/blog/how-we-built-the-figma-plugin-system/): Describes the LowLevelJavascriptVm interface.
187- [An update on plugin security](https://www.figma.com/blog/an-update-on-plugin-security/): Figma switches to QuickJS.
188
189## Status & TODOs
190
191Both the original project quickjs and this project are still in the early stage
192of development.
193There [are tests](https://github.com/justjake/quickjs-emscripten/blob/master/ts/quickjs.test.ts), but I haven't built anything
194on top of this. Please use this project carefully in a production
195environment.
196
197Because the version number of this project is below `1.0.0`, expect occasional
198breaking API changes.
199
200Ideas for future work:
201
202- quickjs-emscripten only exposes a small subset of the QuickJS APIs. Add more QuickJS bindings!
203 - Expose tools for object and array iteration and creation.
204 - Stretch goals: class support, an event emitter bridge implementation
205- Higher-level abstractions for translating values into (and out of) QuickJS.
206- Remove the singleton limitations. Each QuickJS class instance could create
207 its own copy of the emscripten module.
208- Run quickjs-emscripten inside quickjs-emscripten.
209- Remove the `LowLevelJavascriptVm` interface and definition. Those types
210 provide no value, since there is no other implementations, and complicate the
211 types and documentation for quickjs-emscripten.
212- Improve our testing strategy by running the tests with each of the Emscripten santizers, as well as with the SAFE_HEAP. This should catch more bugs in the C code.
213 [See the Emscripten docs for more details](https://emscripten.org/docs/debugging/Sanitizers.html#comparison-to-safe-heap)
214
215## Related
216
217- Duktape wrapped in Wasm: https://github.com/maple3142/duktape-eval/blob/master/src/Makefile
218- QuickJS wrapped in C++: https://github.com/ftk/quickjspp
219
220## Developing
221
222This library is implemented in two languages: C (compiled to WASM with
223Emscripten), and Typescript.
224
225### The C parts
226
227The ./c directory contains C code that wraps the QuickJS C library (in ./quickjs).
228Public functions (those starting with `QTS_`) in ./c/interface.c are
229automatically exported to native code (via a generated header) and to
230Typescript (via a generated FFI class). See ./generate.ts for how this works.
231
232The C code builds as both with `emscripten` (using `emcc`), to produce WASM (or
233ASM.js) and with `clang`. Build outputs are checked in, so
234Intermediate object files from QuickJS end up in ./build/quickjs/{wasm,native}.
235
236This project uses `emscripten 1.39.19`. The install should be handled automatically
237if you're working from Linux or OSX (if using Windows, the best is to use WSL to work
238on this repository). If everything is right, running `yarn embin emcc -v` should print
239something like this:
240
241```
242emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 1.39.18
243clang version 11.0.0 (/b/s/w/ir/cache/git/chromium.googlesource.com-external-github.com-llvm-llvm--project 613c4a87ba9bb39d1927402f4dd4c1ef1f9a02f7)
244```
245
246Related NPM scripts:
247
248- `yarn update-quickjs` will sync the ./quickjs folder with a
249 github repo tracking the upstream QuickJS.
250- `yarn make-debug` will rebuild C outputs into ./build/wrapper
251- `yarn run-n` builds and runs ./c/test.c
252
253### The Typescript parts
254
255The ./ts directory contains Typescript types and wraps the generated Emscripten
256FFI in a more usable interface.
257
258You'll need `node` and `npm` or `yarn`. Install dependencies with `npm install`
259or `yarn install`.
260
261- `yarn build` produces ./dist.
262- `yarn test` runs the tests.
263- `yarn test --watch` watches for changes and re-runs the tests.
264
265### Yarn updates
266
267Just run `yarn set version from sources` to upgrade the Yarn release.