Skip to content

Commit 7a31490

Browse files
committed
src: expose node::MakeContextify to make a node managed vm context
1 parent 3db2206 commit 7a31490

File tree

7 files changed

+283
-1
lines changed

7 files changed

+283
-1
lines changed

src/api/environment.cc

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
#include "node.h"
77
#include "node_builtins.h"
88
#include "node_context_data.h"
9+
#include "node_contextify.h"
910
#include "node_debug.h"
1011
#include "node_errors.h"
1112
#include "node_exit_code.h"
@@ -1057,6 +1058,66 @@ Maybe<bool> InitializeContext(Local<Context> context) {
10571058
return Just(true);
10581059
}
10591060

1061+
ContextifyOptions::ContextifyOptions(Local<String> name,
1062+
Local<String> origin,
1063+
bool allow_code_gen_strings,
1064+
bool allow_code_gen_wasm,
1065+
MicrotaskMode microtask_mode)
1066+
: name_(name),
1067+
origin_(origin),
1068+
allow_code_gen_strings_(allow_code_gen_strings),
1069+
allow_code_gen_wasm_(allow_code_gen_wasm),
1070+
microtask_mode_(microtask_mode) {}
1071+
1072+
MaybeLocal<Context> MakeContextify(Environment* env,
1073+
Local<Object> context_object,
1074+
const ContextifyOptions& options) {
1075+
Isolate* isolate = env->isolate();
1076+
EscapableHandleScope scope(isolate);
1077+
std::unique_ptr<v8::MicrotaskQueue> microtask_queue;
1078+
if (options.microtask_mode() ==
1079+
ContextifyOptions::MicrotaskMode::kAfterEvaluate) {
1080+
microtask_queue = v8::MicrotaskQueue::New(env->isolate(),
1081+
v8::MicrotasksPolicy::kExplicit);
1082+
}
1083+
1084+
contextify::ContextOptions ctxOptions{
1085+
.name = options.name(),
1086+
.origin = options.origin(),
1087+
.allow_code_gen_strings =
1088+
Boolean::New(isolate, options.allow_code_gen_strings()),
1089+
.allow_code_gen_wasm =
1090+
Boolean::New(isolate, options.allow_code_gen_wasm()),
1091+
.own_microtask_queue = std::move(microtask_queue),
1092+
.host_defined_options_id = env->vm_dynamic_import_no_callback(),
1093+
.vanilla = context_object.IsEmpty(),
1094+
};
1095+
1096+
TryCatchScope try_catch(env);
1097+
contextify::ContextifyContext* context_ptr =
1098+
contextify::ContextifyContext::New(env, context_object, &ctxOptions);
1099+
1100+
if (try_catch.HasCaught()) {
1101+
if (!try_catch.HasTerminated()) try_catch.ReThrow();
1102+
// Allocation failure, maximum call stack size reached, termination, etc.
1103+
return {};
1104+
}
1105+
1106+
return scope.Escape(context_ptr->context());
1107+
}
1108+
1109+
MaybeLocal<Context> GetContextified(Environment* env,
1110+
Local<Object> context_object) {
1111+
Isolate* isolate = env->isolate();
1112+
EscapableHandleScope scope(isolate);
1113+
contextify::ContextifyContext* context_ptr =
1114+
contextify::ContextifyContext::Get(context_object);
1115+
if (context_ptr == nullptr) {
1116+
return {};
1117+
}
1118+
return scope.Escape(context_ptr->context());
1119+
}
1120+
10601121
uv_loop_t* GetCurrentEventLoop(Isolate* isolate) {
10611122
HandleScope handle_scope(isolate);
10621123
Local<Context> context = isolate->GetCurrentContext();

src/node.h

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,44 @@ NODE_EXTERN v8::Local<v8::Context> NewContext(
571571
// Return value indicates success of operation
572572
NODE_EXTERN v8::Maybe<bool> InitializeContext(v8::Local<v8::Context> context);
573573

574+
class ContextifyOptions {
575+
public:
576+
enum class MicrotaskMode {
577+
kDefault,
578+
kAfterEvaluate,
579+
};
580+
581+
ContextifyOptions(v8::Local<v8::String> name,
582+
v8::Local<v8::String> origin,
583+
bool allow_code_gen_strings,
584+
bool allow_code_gen_wasm,
585+
MicrotaskMode microtask_mode);
586+
587+
v8::Local<v8::String> name() const { return name_; }
588+
v8::Local<v8::String> origin() const { return origin_; }
589+
bool allow_code_gen_strings() const { return allow_code_gen_strings_; }
590+
bool allow_code_gen_wasm() const { return allow_code_gen_wasm_; }
591+
MicrotaskMode microtask_mode() const { return microtask_mode_; }
592+
593+
private:
594+
v8::Local<v8::String> name_;
595+
v8::Local<v8::String> origin_;
596+
bool allow_code_gen_strings_;
597+
bool allow_code_gen_wasm_;
598+
MicrotaskMode microtask_mode_;
599+
};
600+
601+
// Create a Node.js managed v8::Context with the `context_object`. If the
602+
// `contextObject` is an empty handle, the v8::Context is created without
603+
// wrapping its global object with an object in a Node.js-specific manner.
604+
// The created context is supported in Node.js inspector.
605+
NODE_EXTERN v8::MaybeLocal<v8::Context> MakeContextify(
606+
Environment* env,
607+
v8::Local<v8::Object> context_object,
608+
const ContextifyOptions& options);
609+
NODE_EXTERN v8::MaybeLocal<v8::Context> GetContextified(
610+
Environment* env, v8::Local<v8::Object> context_object);
611+
574612
// If `platform` is passed, it will be used to register new Worker instances.
575613
// It can be `nullptr`, in which case creating new Workers inside of
576614
// Environments that use this `IsolateData` will not work.

src/node_contextify.h

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,10 +131,11 @@ class ContextifyContext final : CPPGC_MIXIN(ContextifyContext) {
131131

132132
static void InitializeGlobalTemplates(IsolateData* isolate_data);
133133

134-
private:
135134
static ContextifyContext* New(Environment* env,
136135
v8::Local<v8::Object> sandbox_obj,
137136
ContextOptions* options);
137+
138+
private:
138139
// Initialize a context created from CreateV8Context()
139140
static ContextifyContext* New(v8::Local<v8::Context> ctx,
140141
Environment* env,
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
#include <node.h>
2+
#include <v8.h>
3+
4+
namespace {
5+
6+
using v8::Context;
7+
using v8::FunctionCallbackInfo;
8+
using v8::HandleScope;
9+
using v8::Isolate;
10+
using v8::Local;
11+
using v8::Object;
12+
using v8::Script;
13+
using v8::String;
14+
using v8::Value;
15+
16+
void MakeContext(const FunctionCallbackInfo<Value>& args) {
17+
Isolate* isolate = Isolate::GetCurrent();
18+
HandleScope handle_scope(isolate);
19+
Local<Context> context = isolate->GetCurrentContext();
20+
node::Environment* env = node::GetCurrentEnvironment(context);
21+
assert(env);
22+
23+
node::ContextifyOptions options(
24+
String::NewFromUtf8Literal(isolate, "Addon Context"),
25+
String::NewFromUtf8Literal(isolate, "addon://about"),
26+
false,
27+
false,
28+
node::ContextifyOptions::MicrotaskMode::kDefault);
29+
// Create a new context with Node.js-specific vm setup.
30+
v8::MaybeLocal<Context> maybe_context =
31+
node::MakeContextify(env, {}, options);
32+
v8::Local<Context> vm_context;
33+
if (!maybe_context.ToLocal(&vm_context)) {
34+
return;
35+
}
36+
37+
// Return the global proxy object.
38+
args.GetReturnValue().Set(vm_context->Global());
39+
}
40+
41+
void RunInContext(const FunctionCallbackInfo<Value>& args) {
42+
Isolate* isolate = Isolate::GetCurrent();
43+
HandleScope handle_scope(isolate);
44+
Local<Context> context = isolate->GetCurrentContext();
45+
node::Environment* env = node::GetCurrentEnvironment(context);
46+
assert(env);
47+
48+
Local<Object> sandbox_obj = args[0].As<Object>();
49+
v8::MaybeLocal<Context> maybe_context =
50+
node::GetContextified(env, sandbox_obj);
51+
v8::Local<Context> vm_context;
52+
if (!maybe_context.ToLocal(&vm_context)) {
53+
return;
54+
}
55+
Context::Scope context_scope(vm_context);
56+
57+
if (args.Length() < 2 || !args[1]->IsString()) {
58+
return;
59+
}
60+
Local<String> source = args[1].As<String>();
61+
Local<Script> script;
62+
Local<Value> result;
63+
64+
if (Script::Compile(vm_context, source).ToLocal(&script) &&
65+
script->Run(vm_context).ToLocal(&result)) {
66+
args.GetReturnValue().Set(result);
67+
}
68+
}
69+
70+
void Initialize(Local<Object> exports,
71+
Local<Value> module,
72+
Local<Context> context) {
73+
NODE_SET_METHOD(exports, "makeContext", MakeContext);
74+
NODE_SET_METHOD(exports, "runInContext", RunInContext);
75+
}
76+
77+
} // anonymous namespace
78+
79+
NODE_MODULE_CONTEXT_AWARE(NODE_GYP_MODULE_NAME, Initialize)
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
{
2+
'targets': [
3+
{
4+
'target_name': 'binding',
5+
'sources': ['binding.cc'],
6+
'includes': ['../common.gypi'],
7+
},
8+
]
9+
}
Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
'use strict';
2+
3+
const common = require('../../common');
4+
common.skipIfInspectorDisabled();
5+
6+
const assert = require('node:assert');
7+
const { once } = require('node:events');
8+
const { Session } = require('node:inspector');
9+
10+
const binding = require(`./build/${common.buildType}/binding`);
11+
12+
const session = new Session();
13+
session.connect();
14+
15+
(async function() {
16+
const mainContextPromise =
17+
once(session, 'Runtime.executionContextCreated');
18+
session.post('Runtime.enable', assert.ifError);
19+
await mainContextPromise;
20+
21+
// Addon-created context should be reported to the inspector.
22+
{
23+
const addonContextPromise =
24+
once(session, 'Runtime.executionContextCreated');
25+
26+
const ctx = binding.makeContext();
27+
const result = binding.runInContext(ctx, '1 + 1');
28+
assert.strictEqual(result, 2);
29+
30+
const { 0: contextCreated } = await addonContextPromise;
31+
const { name, origin, auxData } = contextCreated.params.context;
32+
assert.strictEqual(name, 'Addon Context',
33+
JSON.stringify(contextCreated));
34+
assert.strictEqual(origin, 'addon://about',
35+
JSON.stringify(contextCreated));
36+
assert.strictEqual(auxData.isDefault, false,
37+
JSON.stringify(contextCreated));
38+
}
39+
40+
// `debugger` statement should pause in addon-created context.
41+
{
42+
session.post('Debugger.enable', assert.ifError);
43+
44+
const pausedPromise = once(session, 'Debugger.paused');
45+
const ctx = binding.makeContext();
46+
binding.runInContext(ctx, 'debugger');
47+
await pausedPromise;
48+
49+
session.post('Debugger.resume');
50+
}
51+
})().then(common.mustCall());
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
'use strict';
2+
3+
const common = require('../../common');
4+
const assert = require('assert');
5+
const vm = require('vm');
6+
7+
const binding = require(`./build/${common.buildType}/binding`);
8+
9+
// This verifies that the addon-created context has an independent
10+
// global object.
11+
{
12+
const ctx = binding.makeContext();
13+
const result = binding.runInContext(ctx, `
14+
globalThis.foo = 'bar';
15+
foo;
16+
`);
17+
assert.strictEqual(result, 'bar');
18+
assert.strictEqual(globalThis.foo, undefined);
19+
20+
// Verifies that eval can be disabled in the addon-created context.
21+
assert.throws(() => binding.runInContext(ctx, `
22+
eval('"foo"');
23+
`), { name: 'EvalError' });
24+
25+
// Verifies that the addon-created context does not setup import loader.
26+
const p = binding.runInContext(ctx, `
27+
const p = import('node:fs');
28+
p;
29+
`);
30+
p.catch(common.mustCall((e) => {
31+
assert.throws(() => { throw e; }, { code: 'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING' });
32+
}));
33+
}
34+
35+
36+
// Verifies that the addon can unwrap the context from a vm context
37+
{
38+
const ctx = vm.createContext(vm.constants.DONT_CONTEXTIFY);
39+
vm.runInContext('globalThis.foo = {}', ctx);
40+
const result = binding.runInContext(ctx, 'globalThis.foo');
41+
// The return value identities should be equal.
42+
assert.strictEqual(result, vm.runInContext('globalThis.foo', ctx));
43+
}

0 commit comments

Comments
 (0)