quickjs-emscripten
Module quickjs-emscripten
wraps QuickJS, a modern Javascript interpreter
written in C, for usage from Typescript or Javascript. This allows evaluating
untrusted Javascript safely, or even building a plugin system.
import { getInstance } from 'quickjs-emscripten'
async function main() {
const QuickJS = await getInstance()
const vm = QuickJS.createVm()
const world = vm.createString('world')
vm.setProp(vm.global, 'NAME', world)
world.dispose()
const result = vm.evalCode(`"Hello " + NAME + "!"`)
if (result.error) {
console.log('Execution failed:', vm.dump(result.error))
result.error.dispose()
} else {
console.log('Success:', vm.dump(result.value))
result.value.dispose()
}
vm.dispose()
}
main()
API Documentation | Examples
Usage
Install from npm
: npm install --save quickjs-emscripten
or yarn add quickjs-emscripten
.
The root entrypoint of this library is the getQuickJS
function, which returns
a promise that resolves to a QuickJS singleton when
the Emscripten WASM module is ready.
Safely evaluate Javascript code
See QuickJS.evalCode
import { getQuickJS } from 'quickjs-emscripten'
getQuickJS.then(QuickJS => {
console.log(QuickJS.evalCode('1 + 1'))
})
Note: this will not protect you from infinite loops.
Interfacing with the interpreter
You can use QuickJSVm
to build a scripting environment by modifying globals and exposing functions
into the QuickJS interpreter.
const vm = QuickJS.createVm()
let state = 0
const fnHandle = vm.newFunction('nextId', () => {
return vm.newNumber(++state)
})
vm.setProp(vm.global, 'nextId', fnHandle)
fnHandle.dispose()
const nextId = vm.unwrapResult(vm.evalCode(`nextId(); nextId(); nextId()`))
console.log('vm result:', vm.getNumber(nextId), 'native state:', state)
Background
This was inspired by seeing https://github.com/maple3142/duktape-eval
on Hacker News and Figma's
blogposts about using building a Javascript plugin runtime:
Status
Beta. There are tests, but I haven't built anything
on top of this.
Ideas for future work:
- Simplify memory management. Currently the user must call
handle.dispose()
on all handles they
create to avoid leaking memory in C.
- We chould use a Pool abstraction and do a Pool.freeAll() to free all handles and pointers
in the pool.
- Pools, etc, should not pollute QuickJSVm interface. Composition!
- Expose QuickJS interpreter execution hooks to protect against infinite loops.
- Higher-level abstractions for translating values into (and out of) QuickJS.
These should be implemented in a way that works for any
LowLevelJavascriptVm
implementation. - Removing the singleton limitations. Each QuickJS class instance could create
its own copy of the emscripten module, although we'd need to make all public
methods async - so they wait for the module instance to be ready.
Related
Developing
This library is implemented in two languages: C (compiled to WASM with
Emscripten), and Typescript. Emscripten outputs are checked in, so you will
only need the C compiler if you need to modify C code.
The C parts
The ./c directory contains C code that wraps the QuickJS C library (in ./quickjs).
Public functions (those starting with QTS_
) in ./c/interface.c are
automatically exported to native code (via a generated header) and to
Typescript (via a generated FFI class). See ./generate.ts for how this works.
The C code builds as both with emscripten
(using emcc
), to produce WASM (or
ASM.js) and with clang
. Build outputs are checked in, so
Intermediate object files from QuickJS end up in ./build/quickjs/{wasm,native}.
You'll need to install emscripten
. Following the offical instructions here, using emsdk
:
https://emscripten.org/docs/getting_started/downloads.html#installation-instructions
Related NPM scripts:
yarn update-quickjs
will sync the ./quickjs folder with a
github repo tracking the upstream QuickJS.yarn make-debug
will rebuild C outputs into ./build/wrapperyarn run-n
builds and runs ./c/test.c
The Typescript parts
The ./ts directory contains Typescript types and wraps the generated Emscripten
FFI in a more usable interface.
You'll need node
and npm
or yarn
. Install dependencies with npm install
or yarn install
.
yarn build
produces ./dist.yarn test
runs the tests.yarn test --watch
watches for changes and re-runs the tests.