1 | # quickjs-emscripten
|
2 |
|
3 | Javascript/Typescript bindings for [QuickJS, a modern Javascript interpreter written in
|
4 | C 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
|
11 | import { getQuickJS } from 'quickjs-emscripten'
|
12 |
|
13 | async 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 |
|
33 | main()
|
34 | ```
|
35 |
|
36 | ## Usage
|
37 |
|
38 | Install from `npm`: `npm install --save quickjs-emscripten` or `yarn add quickjs-emscripten`.
|
39 |
|
40 | The root entrypoint of this library is the `getQuickJS` function, which returns
|
41 | a promise that resolves to a [QuickJS singleton](doc/classes/quickjs.md) when
|
42 | the Emscripten WASM module is ready.
|
43 |
|
44 | Once `getQuickJS` has been awaited at least once, you also can use the `getQuickJSSync`
|
45 | function to directly access the singleton engine in your synchronous code.
|
46 |
|
47 | ### Safely evaluate Javascript code
|
48 |
|
49 | See [QuickJS.evalCode](https://github.com/justjake/quickjs-emscripten/blob/master/doc/classes/quickjs.md#evalcode)
|
50 |
|
51 | ```typescript
|
52 | import { getQuickJS, shouldInterruptAfterDeadline } from 'quickjs-emscripten'
|
53 |
|
54 | getQuickJS().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 |
|
65 | You can use [QuickJSVm](https://github.com/justjake/quickjs-emscripten/blob/master/doc/classes/quickjsvm.md)
|
66 | to build a scripting environment by modifying globals and exposing functions
|
67 | into the QuickJS interpreter.
|
68 |
|
69 | Each `QuickJSVm` instance has its own environment, CPU limit, and memory
|
70 | limit. See the documentation for details.
|
71 |
|
72 | ```typescript
|
73 | const vm = QuickJS.createVm()
|
74 | let state = 0
|
75 |
|
76 | const fnHandle = vm.newFunction('nextId', () => {
|
77 | return vm.newNumber(++state)
|
78 | })
|
79 |
|
80 | vm.setProp(vm.global, 'nextId', fnHandle)
|
81 | fnHandle.dispose()
|
82 |
|
83 | const nextId = vm.unwrapResult(vm.evalCode(`nextId(); nextId(); nextId()`))
|
84 | console.log('vm result:', vm.getNumber(nextId), 'native state:', state)
|
85 |
|
86 | nextId.dispose()
|
87 | vm.dispose()
|
88 | ```
|
89 |
|
90 | ### Memory Management
|
91 |
|
92 | Many methods in this library return handles to memory allocated inside the
|
93 | WebAssembly heap. These types cannot be garbage-collected as usual in
|
94 | Javascript. Instead, you must manually manage their memory by calling a
|
95 | `.dispose()` method to free the underlying resources. Once a handle has been
|
96 | disposed, 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 |
|
99 | Calling `QuickJSVm.dispose()` will throw a RuntimeError if you've forgotten to
|
100 | dispose any handles associated with that VM, so it's good practice to create a
|
101 | new VM instance for each of your tests, and to call `vm.dispose()` at the end
|
102 | of every test.
|
103 |
|
104 | ```typescript
|
105 | const vm = QuickJS.createVm()
|
106 | const numberHandle = vm.newNumber(42)
|
107 | // Note: numberHandle not disposed, so it leaks memory.
|
108 | vm.dispose()
|
109 | // throws RuntimeError: abort(Assertion failed: list_empty(&rt->gc_obj_list), at: quickjs/quickjs.c,1963,JS_FreeRuntime)
|
110 | ```
|
111 |
|
112 | Here are some strategies to reduce the toil of calling `.dispose()` on each
|
113 | handle you create:
|
114 |
|
115 | #### Scope
|
116 |
|
117 | A
|
118 | [`Scope`](https://github.com/justjake/quickjs-emscripten/blob/master/doc/classes/scope.md#class-scope)
|
119 | instance manages a set of disposables and calls their `.dispose()`
|
120 | method 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
|
124 | Scope.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 |
|
144 | You can also create `Scope` instances with `new Scope()` if you want to manage
|
145 | calling `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)
|
150 | is sugar for the common pattern of using a handle and then
|
151 | immediately disposing of it. `Lifetime.consume` takes a `map` function that
|
152 | produces a result of any type. The `map` fuction is called with the handle,
|
153 | then the handle is disposed, then the result is returned.
|
154 |
|
155 | Here's the "Interfacing with interpreter" example re-written using `.consume()`:
|
156 |
|
157 | ```typescript
|
158 | const vm = QuickJS.createVm()
|
159 | let state = 0
|
160 |
|
161 | vm.newFunction('nextId', () => {
|
162 | return vm.newNumber(++state)
|
163 | }).consume(fnHandle => vm.setProp(vm.global, 'nextId', fnHandle))
|
164 |
|
165 | vm.unwrapResult(vm.evalCode(`nextId(); nextId(); nextId()`)).consume(nextId =>
|
166 | console.log('vm result:', vm.getNumber(nextId), 'native state:', state)
|
167 | )
|
168 |
|
169 | vm.dispose()
|
170 | ```
|
171 |
|
172 | Generally 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 |
|
182 | This was inspired by seeing https://github.com/maple3142/duktape-eval
|
183 | [on Hacker News](https://news.ycombinator.com/item?id=21946565) and Figma's
|
184 | blogposts 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 |
|
191 | Both the original project quickjs and this project are still in the early stage
|
192 | of development.
|
193 | There [are tests](https://github.com/justjake/quickjs-emscripten/blob/master/ts/quickjs.test.ts), but I haven't built anything
|
194 | on top of this. Please use this project carefully in a production
|
195 | environment.
|
196 |
|
197 | Because the version number of this project is below `1.0.0`, expect occasional
|
198 | breaking API changes.
|
199 |
|
200 | Ideas 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 |
|
222 | This library is implemented in two languages: C (compiled to WASM with
|
223 | Emscripten), and Typescript.
|
224 |
|
225 | ### The C parts
|
226 |
|
227 | The ./c directory contains C code that wraps the QuickJS C library (in ./quickjs).
|
228 | Public functions (those starting with `QTS_`) in ./c/interface.c are
|
229 | automatically exported to native code (via a generated header) and to
|
230 | Typescript (via a generated FFI class). See ./generate.ts for how this works.
|
231 |
|
232 | The C code builds as both with `emscripten` (using `emcc`), to produce WASM (or
|
233 | ASM.js) and with `clang`. Build outputs are checked in, so
|
234 | Intermediate object files from QuickJS end up in ./build/quickjs/{wasm,native}.
|
235 |
|
236 | This project uses `emscripten 1.39.19`. The install should be handled automatically
|
237 | if you're working from Linux or OSX (if using Windows, the best is to use WSL to work
|
238 | on this repository). If everything is right, running `yarn embin emcc -v` should print
|
239 | something like this:
|
240 |
|
241 | ```
|
242 | emcc (Emscripten gcc/clang-like replacement + linker emulating GNU ld) 1.39.18
|
243 | clang version 11.0.0 (/b/s/w/ir/cache/git/chromium.googlesource.com-external-github.com-llvm-llvm--project 613c4a87ba9bb39d1927402f4dd4c1ef1f9a02f7)
|
244 | ```
|
245 |
|
246 | Related 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 |
|
255 | The ./ts directory contains Typescript types and wraps the generated Emscripten
|
256 | FFI in a more usable interface.
|
257 |
|
258 | You'll need `node` and `npm` or `yarn`. Install dependencies with `npm install`
|
259 | or `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 |
|
267 | Just run `yarn set version from sources` to upgrade the Yarn release.
|