VM

The vm, or Virtual Machine, module allows you to run arbitrary chunks of code and get a result back. It has a number of features that allow you to change the context in which the code runs. This can be useful to act as a kind of faux sandbox. However, the code is still running in the same Node process, so you should be cautious. vm is similar to eval(), but offers some more features and a better API for managing code. It doesn’t have the ability to interact with the local scope in the way that eval() does, however.

There are two ways to run code with vm. Running the code “inline” is similar to using eval(). The second way is to precompile the code into a vm.Script object. Let’s have a look at Example 5-40, which demonstrates running code inline using vm.

Example 5-40. Using vm to run code

> var vm = require('vm');
> vm.runInThisContext("1+1");
2

So far, vm looks a lot like eval(). We pass some code to it, and we get a result back. However, vm doesn’t interact with local scope in the same way that eval() does. Code run with eval() will behave as if it were truly inline and replaces the eval() method call. But calls to vm methods will not interact with the local scope. So eval() can change the surrounding context, whereas vm cannot, as shown in Example 5-41.

Example 5-41. Accessing the local scope to show the differences between vm and eval( )

> var vm = require('vm'),
... e = 0,
... v = 0;
> eval(e=e+1);
1
> e
1
> vm.runInThisContext('v=v+1');
ReferenceError: v is not defined
    at evalmachine.<anonymous>:1:1
    at [object Context]:1:4
    at Interface.<anonymous> (repl.js:171:22)
    at Interface.emit (events.js:64:17)
    at Interface._onLine (readline.js:153:10)
    at Interface._line (readline.js:408:8)
    at Interface._ttyWrite (readline.js:585:14)
    at ReadStream.<anonymous> (readline.js:73:12)
    at ReadStream.emit (events.js:81:20)
    at ReadStream._emitKey (tty_posix.js:307:10)
>
> vm.runInThisContext('v=0');
0
> vm.runInThisContext('v=v+1');
1
>
0

We’ve created two variables, e and v. When we use the e variable with eval(), the end result of the statement applies back to the main context. However, when we try the same thing with v and vm.runInThisContext(), we get an exception because we refer to v on the right side of the equals sign, and that variable is not defined. Whereas eval() runs in the local scope, vm does not.

The vm subsystem actually maintains its own local context that persists from one invocation of vm to another. Thus, if we create v within the scope of the vm, the variable subsequently is available to later vm invocations, maintaining the state in which the first vm left it. However, the variable from the vm has no impact on v in the local scope of the main event loop.

It’s also possible to pass a preexisting context to vm. This context will be used in place of the default context.

Example 5-42 uses vm.runInNewContext(), which takes a context object as a second argument. The scope of that object becomes the context for the code we run with vm. If we continue to pass it from object to object, the context will be modified. However, the context is also available to the global scope.

Example 5-42. Passing a context in to vm

> var vm = require('vm');
> var context = { alphabet:"" };
> vm.runInNewContext("alphabet+='a'", context);
'a'
> vm.runInNewContext("alphabet+='b'", context);
'ab'
> context
{ alphabet: 'ab' }
>

You can also compile vm.Script objects (Example 5-43). These save a piece of code that you can then run repeatedly. At runtime, you can choose the context to be applied. This is helpful when you are repeatedly running the same code against multiple contexts.

Example 5-43. Compiling code into a script with vm

> var vm = require('vm');
> var fs = require('fs');
> 
> var code = fs.readFileSync('example.js');
> code.toString();
'console.log(output);\n'
>
> var script = vm.createScript(code);
> script.runInNewContext({output:"Kick Ass"});
ReferenceError: console is not defined
    at undefined:1:1
    at [object Context]:1:8
    at Interface.<anonymous> (repl.js:171:22)
    at Interface.emit (events.js:64:17)
    at Interface._onLine (readline.js:153:10)
    at Interface._line (readline.js:408:8)
    at Interface._ttyWrite (readline.js:585:14)
    at ReadStream.<anonymous> (readline.js:73:12)
    at ReadStream.emit (events.js:81:20)
    at ReadStream._emitKey (tty_posix.js:307:10)
> script.runInNewContext({"console":console,"output":"Kick Ass"});
Kick Ass

This example reads in a JavaScript file that contains the simple command console.log(output);. we compile this into a script object, which means we can then run script.runInNewContext() on the script and pass in a context. We deliberately triggered an error to show that, just as when running vm.runInNewContext(), you need to pass in the objects to which you refer (such as the console object); otherwise, even basic global functions are not available. It’s also worth noting that the exception is thrown from undefined:1:1.

All the vm run commands take a filename as an optional final argument. It doesn’t change the functionality, but allows you to set the name of the file that appears in a message when an error is thrown. This is useful if you load a lot of files from disk and run them because it tells you which piece of code threw an error. The parameter is totally arbitrary, so you could use whatever string is meaningful to help you debug the code.

Get Node: Up and Running now with the O’Reilly learning platform.

O’Reilly members experience books, live events, courses curated by job role, and more from O’Reilly and nearly 200 top publishers.