Javascript bytecode compiler/vm written in CoffeeScript.
npm install vm.js
There are about 200 acceptance tests that are run twice: once inside a normal Vm and again inside a self-hosted Vm(think about the movie 'Inception' for an easy analogy). That means it is capable of running moderately complex javascript code(since it runs itself plus the esprima parser), so it should be stable enough for most uses.
npm test
vm.js implements a ECMAScript virtual machine that can be used from any ECMAScript3-compatible environment. Eventually it will provide a complete ECMAScript 6 environment(for now only some features are supported)
Here are some possible use cases:
- Simple in-process javascript sandbox
- Async-to-sync API adapter using fibers (lightweight in-process threads)
- Use new ECMAScript features in very old browsers
The main API can be accessed through Vm instances. Each Vm is indirectly associated with a global object(through a Realm) and is isolated from other Vms.
Start by creating a new instance:
> Vm = require('vm.js')
> vm = new Vm()
Evaluate simple expressions:
> vm.eval('40 + 2')
42
> vm.eval('[a, b, c] = [1, 2, 3]') // Harmony destructuring assignment
[1, 2, 3]
> vm.realm.global.a
1
> vm.realm.global.b
2
> vm.realm.global.c
3
Compile programs and run later
// pass filename as second argument for stack traces/debugging
> script = Vm.compile('2 + 2', 'sum.js')
> vm.run(script)
4
Compiled scripts can be serialized/deserialized to/from JSON-friendly structures:
> scriptObj = script.toJSON()
> serializedScript = JSON.stringify(scriptObj)
> deserializedScript = Vm.fromJSON(JSON.parse(serializedScript))
> vm.run(deserializedScript)
Expose objects to be used by code running inside the Vm
> vm.realm.global.factorial = function factorial(n) { return n > 1 ? factorial(n - 1) * n : 1 }
> vm.eval('factorial(5)')
120
The inverse also works:
> vm.eval('function factorial(n) { return n > 1 ? factorial(n - 1) * n : 1 }')
> vm.realm.global.factorial(5)
120
Return values asynchronously using fiber pause/resume:
// created a paused fiber from compiled code
fiber = vm.createFiber(Vm.compile('user = null; user = fetchAsync("/users/1");'))
vm.realm.global.fetchAsync = function(url) {
fiber.pause() // pause execution
$.get(url, function(data) {
// user === null
fiber.setReturnValue(data)
fiber.resume()
// user === data
});
}
// start fiber
fiber.run()
Builtin objects are exposed, but each Vm instance uses a copy-on-write algorithm to maintain its own state of builtin globals:
> vm.eval('Object.prototype') === Object.prototype
true
> vm.eval('Object.prototype.bark = function() { return "bark!" }')
> vm.eval('[].bark()')
'bark!'
> 'bark' in Object.prototype
false
Its possible to provide a instruction timeout(maximum number of instructions a fiber can execute).
> vm.eval("i = 0; while (true) i++", 'timeout.js', 500)
TimeoutError: Script timed out
at timeout.js:1:20
The TimeoutError has a 'fiber' property that references the paused fiber(it can be resumed, optionally passing another timeout to the 'resume' method).
As shown above, some harmony features are available. Here's a generator example:
> vm.eval(
'function* fib() {' +
'var i = 0, j = 1, t;' +
'while (true) {' +
'yield i;' +
't = i;' +
'i = j;' +
'j += t;' +
'}' +
'}')
> vm.eval('l = []')
> vm.eval('for (let i of fib()) {if (l.length > 10) break; l.push(i)}')
> vm.eval('l')
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
Like normal functions, the 'fib' generator can be used outside a vm instance:
> g = vm.realm.global.fib(); l = [];
> while (l.length < 11) l.push(g.next())
> l
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34, 55]
See more examples and supported features here.
This was inspired by Continuum which is a ECMAScript 6 virtual machine implemented in pure javascript. I wrote this because I wanted a smaller codebase(less than 5k lines of CoffeeScript code) and seamless integration with the host virtual machine(no need to call strange internal methods to access objects manipulated by the vm).
For parsing source code this library uses the esprima parser.