Skip to content

tarruda/vm.js

Repository files navigation

vm.js

Javascript bytecode compiler/vm written in CoffeeScript.

Dependency status Build Status

Installation

npm install vm.js

Tests

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

Overview

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

Usage

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.

Comments

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.