Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

vm: introduce vanilla contexts via vm.constants.DONT_CONTEXTIFY #54394

Merged
merged 4 commits into from
Aug 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
166 changes: 143 additions & 23 deletions doc/api/vm.md
Original file line number Diff line number Diff line change
Expand Up @@ -229,6 +229,9 @@ overhead.
<!-- YAML
added: v0.3.1
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/54394
description: The `contextObject` argument now accepts `vm.constants.DONT_CONTEXTIFY`.
- version: v14.6.0
pr-url: https://github.com/nodejs/node/pull/34023
description: The `microtaskMode` option is supported now.
Expand All @@ -240,8 +243,9 @@ changes:
description: The `breakOnSigint` option is supported now.
-->

* `contextObject` {Object} An object that will be [contextified][]. If
`undefined`, a new object will be created.
* `contextObject` {Object|vm.constants.DONT\_CONTEXTIFY|undefined}
Either [`vm.constants.DONT_CONTEXTIFY`][] or an object that will be [contextified][].
If `undefined`, an empty contextified object will be created for backwards compatibility.
* `options` {Object}
* `displayErrors` {boolean} When `true`, if an [`Error`][] occurs
while compiling the `code`, the line of code causing the error is attached
Expand Down Expand Up @@ -275,9 +279,16 @@ changes:
`breakOnSigint` scopes in that case.
* Returns: {any} the result of the very last statement executed in the script.

First contextifies the given `contextObject`, runs the compiled code contained
by the `vm.Script` object within the created context, and returns the result.
Running code does not have access to local scope.
This method is a shortcut to `script.runInContext(vm.createContext(options), options)`.
It does several things at once:

1. Creates a new context.
2. If `contextObject` is an object, [contextifies][contextified] it with the new context.
If `contextObject` is undefined, creates a new object and [contextifies][contextified] it.
If `contextObject` is [`vm.constants.DONT_CONTEXTIFY`][], don't [contextify][contextified] anything.
3. Runs the compiled code contained by the `vm.Script` object within the created context. The code
does not have access to the scope in which this method is called.
4. Returns the result.

The following example compiles code that sets a global variable, then executes
the code multiple times in different contexts. The globals are set on and
Expand All @@ -295,6 +306,12 @@ contexts.forEach((context) => {

console.log(contexts);
// Prints: [{ globalVar: 'set' }, { globalVar: 'set' }, { globalVar: 'set' }]

// This would throw if the context is created from a contextified object.
// vm.constants.DONT_CONTEXTIFY allows creating contexts with ordinary
// global objects that can be frozen.
const freezeScript = new vm.Script('Object.freeze(globalThis); globalThis;');
const frozenContext = freezeScript.runInNewContext(vm.constants.DONT_CONTEXTIFY);
```

### `script.runInThisContext([options])`
Expand Down Expand Up @@ -1072,6 +1089,10 @@ For detailed information, see
<!-- YAML
added: v0.3.1
changes:
- version:
- REPLACEME
pr-url: https://github.com/nodejs/node/pull/54394
description: The `contextObject` argument now accepts `vm.constants.DONT_CONTEXTIFY`.
- version:
- v21.7.0
- v20.12.0
Expand All @@ -1094,7 +1115,9 @@ changes:
description: The `codeGeneration` option is supported now.
-->

* `contextObject` {Object}
* `contextObject` {Object|vm.constants.DONT\_CONTEXTIFY|undefined}
Either [`vm.constants.DONT_CONTEXTIFY`][] or an object that will be [contextified][].
If `undefined`, an empty contextified object will be created for backwards compatibility.
* `options` {Object}
* `name` {string} Human-readable name of the newly created context.
**Default:** `'VM Context i'`, where `i` is an ascending numerical index of
Expand Down Expand Up @@ -1124,10 +1147,10 @@ changes:
[Support of dynamic `import()` in compilation APIs][].
* Returns: {Object} contextified object.
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved

If given a `contextObject`, the `vm.createContext()` method will [prepare that
If the given `contextObject` is an object, the `vm.createContext()` method will [prepare that
object][contextified] and return a reference to it so that it can be used in
calls to [`vm.runInContext()`][] or [`script.runInContext()`][]. Inside such
scripts, the `contextObject` will be the global object, retaining all of its
scripts, the global object will be wrapped by the `contextObject`, retaining all of its
existing properties but also having the built-in objects and functions any
standard [global object][] has. Outside of scripts run by the vm module, global
variables will remain unchanged.
Expand All @@ -1152,6 +1175,11 @@ console.log(global.globalVar);
If `contextObject` is omitted (or passed explicitly as `undefined`), a new,
empty [contextified][] object will be returned.

When the global object in the newly created context is [contextified][], it has some quirks
compared to ordinary global objects. For example, it cannot be frozen. To create a context
without the contextifying quirks, pass [`vm.constants.DONT_CONTEXTIFY`][] as the `contextObject`
argument. See the documentation of [`vm.constants.DONT_CONTEXTIFY`][] for details.

The `vm.createContext()` method is primarily useful for creating a single
context that can be used to run multiple scripts. For instance, if emulating a
web browser, the method can be used to create a single context representing a
Expand All @@ -1171,7 +1199,8 @@ added: v0.11.7
* Returns: {boolean}

Returns `true` if the given `object` object has been [contextified][] using
[`vm.createContext()`][].
[`vm.createContext()`][], or if it's the global object of a context created
using [`vm.constants.DONT_CONTEXTIFY`][].

## `vm.measureMemory([options])`

Expand Down Expand Up @@ -1332,6 +1361,10 @@ console.log(contextObject);
<!-- YAML
added: v0.3.1
changes:
- version:
- REPLACEME
pr-url: https://github.com/nodejs/node/pull/54394
description: The `contextObject` argument now accepts `vm.constants.DONT_CONTEXTIFY`.
- version:
- v21.7.0
- v20.12.0
Expand All @@ -1356,8 +1389,9 @@ changes:
-->

* `code` {string} The JavaScript code to compile and run.
* `contextObject` {Object} An object that will be [contextified][]. If
`undefined`, a new object will be created.
* `contextObject` {Object|vm.constants.DONT\_CONTEXTIFY|undefined}
Either [`vm.constants.DONT_CONTEXTIFY`][] or an object that will be [contextified][].
If `undefined`, an empty contextified object will be created for backwards compatibility.
* `options` {Object|string}
* `filename` {string} Specifies the filename used in stack traces produced
by this script. **Default:** `'evalmachine.<anonymous>'`.
Expand Down Expand Up @@ -1407,13 +1441,21 @@ changes:
`breakOnSigint` scopes in that case.
* Returns: {any} the result of the very last statement executed in the script.

The `vm.runInNewContext()` first contextifies the given `contextObject` (or
creates a new `contextObject` if passed as `undefined`), compiles the `code`,
runs it within the created context, then returns the result. Running code
does not have access to the local scope.

This method is a shortcut to
`(new vm.Script(code, options)).runInContext(vm.createContext(options), options)`.
If `options` is a string, then it specifies the filename.

It does several things at once:

1. Creates a new context.
2. If `contextObject` is an object, [contextifies][contextified] it with the new context.
If `contextObject` is undefined, creates a new object and [contextifies][contextified] it.
If `contextObject` is [`vm.constants.DONT_CONTEXTIFY`][], don't [contextify][contextified] anything.
3. Compiles the code as a`vm.Script`
4. Runs the compield code within the created context. The code does not have access to the scope in
which this method is called.
5. Returns the result.

The following example compiles and executes code that increments a global
variable and sets a new one. These globals are contained in the `contextObject`.

Expand All @@ -1428,6 +1470,11 @@ const contextObject = {
vm.runInNewContext('count += 1; name = "kitty"', contextObject);
console.log(contextObject);
// Prints: { animal: 'cat', count: 3, name: 'kitty' }

// This would throw if the context is created from a contextified object.
// vm.constants.DONT_CONTEXTIFY allows creating contexts with ordinary global objects that
// can be frozen.
const frozenContext = vm.runInNewContext('Object.freeze(globalThis); globalThis;', vm.constants.DONT_CONTEXTIFY);
```

## `vm.runInThisContext(code[, options])`
Expand Down Expand Up @@ -1555,13 +1602,85 @@ According to the [V8 Embedder's Guide][]:
> JavaScript applications to run in a single instance of V8. You must explicitly
> specify the context in which you want any JavaScript code to be run.

When the method `vm.createContext()` is called, the `contextObject` argument
(or a newly-created object if `contextObject` is `undefined`) is associated
internally with a new instance of a V8 Context. This V8 Context provides the
`code` run using the `node:vm` module's methods with an isolated global
environment within which it can operate. The process of creating the V8 Context
and associating it with the `contextObject` is what this document refers to as
"contextifying" the object.
When the method `vm.createContext()` is called with an object, the `contextObject` argument
will be used to wrap the global object of a new instance of a V8 Context
(if `contextObject` is `undefined`, a new object will be created from the current context
before its contextified). This V8 Context provides the `code` run using the `node:vm`
module's methods with an isolated global environment within which it can operate.
The process of creating the V8 Context and associating it with the `contextObject`
in the outer context is what this document refers to as "contextifying" the object.

The contextifying would introduce some quirks to the `globalThis` value in the context.
For example, it cannot be frozen, and it is not reference equal to the `contextObject`
in the outer context.

```js
const vm = require('node:vm');

// An undefined `contextObject` option makes the global object contextified.
let context = vm.createContext();
console.log(vm.runInContext('globalThis', context) === context); // false
// A contextified global object cannot be frozen.
try {
vm.runInContext('Object.freeze(globalThis);', context);
} catch(e) {
console.log(e); // TypeError: Cannot freeze
}
console.log(vm.runInContext('globalThis.foo = 1; foo;', context)); // 1
```

To create a context with an ordinary global object and get access to a global proxy in
the outer context with fewer quirks, specify `vm.constants.DONT_CONTEXTIFY` as the
`contextObject` argument.

### `vm.constants.DONT_CONTEXTIFY`

This constant, when used as the `contextObject` argument in vm APIs, instructs Node.js to create
a context without wrapping its global object with another object in a Node.js-specific manner.
As a result, the `globalThis` value inside the new context would behave more closely to an ordinary
one.

```js
const vm = require('node:vm');

// Use vm.constants.DONT_CONTEXTIFY to freeze the global object.
const context = vm.createContext(vm.constants.DONT_CONTEXTIFY);
vm.runInContext('Object.freeze(globalThis);', context);
try {
vm.runInContext('bar = 1; bar;', context);
} catch(e) {
console.log(e); // Uncaught ReferenceError: bar is not defined
}
```

When `vm.constants.DONT_CONTEXTIFY` is used as the `contextObject` argument to [`vm.createContext()`][],
the returned object is a proxy-like object to the global object in the newly created context with
joyeecheung marked this conversation as resolved.
Show resolved Hide resolved
fewer Node.js-specific quirks. It is reference equal to the `globalThis` value in the new context,
can be modified from outside the context, and can be used to access built-ins in the new context directly.

```js
const vm = require('node:vm');

const context = vm.createContext(vm.constants.DONT_CONTEXTIFY);

// Returned object is reference equal to globalThis in the new context.
console.log(vm.runInContext('globalThis', context) === context); // true

// Can be used to access globals in the new context directly.
console.log(context.Array); // [Function: Array]
vm.runInContext('foo = 1;', context);
console.log(context.foo); // 1
context.bar = 1;
console.log(vm.runInContext('bar;', context)); // 1

// Can be frozen and it affects the inner context.
Object.freeze(context);
try {
vm.runInContext('baz = 1; baz;', context);
} catch(e) {
console.log(e); // Uncaught ReferenceError: baz is not defined
}
```

## Timeout interactions with asynchronous tasks and Promises

Expand Down Expand Up @@ -1851,6 +1970,7 @@ const { Script, SyntheticModule } = require('node:vm');
[`script.runInThisContext()`]: #scriptruninthiscontextoptions
[`url.origin`]: url.md#urlorigin
[`vm.compileFunction()`]: #vmcompilefunctioncode-params-options
[`vm.constants.DONT_CONTEXTIFY`]: #vmconstantsdont_contextify
[`vm.createContext()`]: #vmcreatecontextcontextobject-options
[`vm.runInContext()`]: #vmrunincontextcode-contextifiedobject-options
[`vm.runInThisContext()`]: #vmruninthiscontextcode-options
Expand Down
10 changes: 6 additions & 4 deletions lib/vm.js
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,7 @@ const {
} = require('internal/vm');
const {
vm_dynamic_import_main_context_default,
vm_context_no_contextify,
} = internalBinding('symbols');
const kParsingContext = Symbol('script parsing context');

Expand Down Expand Up @@ -222,7 +223,7 @@ function getContextOptions(options) {

let defaultContextNameIndex = 1;
function createContext(contextObject = {}, options = kEmptyObject) {
if (isContext(contextObject)) {
if (contextObject !== vm_context_no_contextify && isContext(contextObject)) {
return contextObject;
}

Expand Down Expand Up @@ -258,10 +259,10 @@ function createContext(contextObject = {}, options = kEmptyObject) {
const hostDefinedOptionId =
getHostDefinedOptionId(importModuleDynamically, name);

makeContext(contextObject, name, origin, strings, wasm, microtaskQueue, hostDefinedOptionId);
const result = makeContext(contextObject, name, origin, strings, wasm, microtaskQueue, hostDefinedOptionId);
// Register the context scope callback after the context was initialized.
registerImportModuleDynamically(contextObject, importModuleDynamically);
return contextObject;
registerImportModuleDynamically(result, importModuleDynamically);
return result;
}

function createScript(code, options) {
Expand Down Expand Up @@ -394,6 +395,7 @@ function measureMemory(options = kEmptyObject) {
const vmConstants = {
__proto__: null,
USE_MAIN_CONTEXT_DEFAULT_LOADER: vm_dynamic_import_main_context_default,
DONT_CONTEXTIFY: vm_context_no_contextify,
};

ObjectFreeze(vmConstants);
Expand Down
1 change: 1 addition & 0 deletions src/env_properties.h
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,7 @@
V(resource_symbol, "resource_symbol") \
V(trigger_async_id_symbol, "trigger_async_id_symbol") \
V(source_text_module_default_hdo, "source_text_module_default_hdo") \
V(vm_context_no_contextify, "vm_context_no_contextify") \
V(vm_dynamic_import_default_internal, "vm_dynamic_import_default_internal") \
V(vm_dynamic_import_main_context_default, \
"vm_dynamic_import_main_context_default") \
Expand Down
Loading
Loading