Skip to content

Commit c6a39cf

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

File tree

7 files changed

+232
-1
lines changed

7 files changed

+232
-1
lines changed

src/api/environment.cc

Lines changed: 48 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,53 @@ 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> contextObject,
1074+
const ContextifyOptions& options) {
1075+
Isolate* isolate = env->isolate();
1076+
std::unique_ptr<v8::MicrotaskQueue> microtask_queue;
1077+
if (options.microtask_mode() ==
1078+
ContextifyOptions::MicrotaskMode::kAfterEvaluate) {
1079+
microtask_queue = v8::MicrotaskQueue::New(env->isolate(),
1080+
v8::MicrotasksPolicy::kExplicit);
1081+
}
1082+
1083+
contextify::ContextOptions ctxOptions{
1084+
.name = options.name(),
1085+
.origin = options.origin(),
1086+
.allow_code_gen_strings =
1087+
Boolean::New(isolate, options.allow_code_gen_strings()),
1088+
.allow_code_gen_wasm =
1089+
Boolean::New(isolate, options.allow_code_gen_wasm()),
1090+
.own_microtask_queue = std::move(microtask_queue),
1091+
.host_defined_options_id = env->vm_dynamic_import_no_callback(),
1092+
.vanilla = contextObject.IsEmpty(),
1093+
};
1094+
1095+
TryCatchScope try_catch(env);
1096+
contextify::ContextifyContext* context_ptr =
1097+
contextify::ContextifyContext::New(env, contextObject, &ctxOptions);
1098+
1099+
if (try_catch.HasCaught()) {
1100+
if (!try_catch.HasTerminated()) try_catch.ReThrow();
1101+
// Allocation failure, maximum call stack size reached, termination, etc.
1102+
return {};
1103+
}
1104+
1105+
return context_ptr->context();
1106+
}
1107+
10601108
uv_loop_t* GetCurrentEventLoop(Isolate* isolate) {
10611109
HandleScope handle_scope(isolate);
10621110
Local<Context> context = isolate->GetCurrentContext();

src/node.h

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -571,6 +571,42 @@ 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 `contextObject`. 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> contextObject,
608+
const ContextifyOptions& options);
609+
574610
// If `platform` is passed, it will be used to register new Worker instances.
575611
// It can be `nullptr`, in which case creating new Workers inside of
576612
// 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: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
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 CreateAndRunInContext(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+
Context::Scope context_scope(vm_context);
37+
38+
if (args.Length() == 0 || !args[0]->IsString()) {
39+
return;
40+
}
41+
Local<String> source = args[0].As<String>();
42+
Local<Script> script;
43+
Local<Value> result;
44+
45+
if (Script::Compile(vm_context, source).ToLocal(&script) &&
46+
script->Run(vm_context).ToLocal(&result)) {
47+
args.GetReturnValue().Set(result);
48+
}
49+
}
50+
51+
void Initialize(Local<Object> exports,
52+
Local<Value> module,
53+
Local<Context> context) {
54+
NODE_SET_METHOD(exports, "createAndRunInContext", CreateAndRunInContext);
55+
}
56+
57+
} // anonymous namespace
58+
59+
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: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,49 @@
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 result = binding.createAndRunInContext('1 + 1');
27+
assert.strictEqual(result, 2);
28+
29+
const { 0: contextCreated } = await addonContextPromise;
30+
const { name, origin, auxData } = contextCreated.params.context;
31+
assert.strictEqual(name, 'Addon Context',
32+
JSON.stringify(contextCreated));
33+
assert.strictEqual(origin, 'addon://about',
34+
JSON.stringify(contextCreated));
35+
assert.strictEqual(auxData.isDefault, false,
36+
JSON.stringify(contextCreated));
37+
}
38+
39+
// `debugger` statement should pause in addon-created context.
40+
{
41+
session.post('Debugger.enable', assert.ifError);
42+
43+
const pausedPromise = once(session, 'Debugger.paused');
44+
binding.createAndRunInContext('debugger');
45+
await pausedPromise;
46+
47+
session.post('Debugger.resume');
48+
}
49+
})().then(common.mustCall());
Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
'use strict';
2+
3+
const common = require('../../common');
4+
const assert = require('assert');
5+
6+
const binding = require(`./build/${common.buildType}/binding`);
7+
8+
// This verifies that the addon-created context has an independent
9+
// global object.
10+
const result = binding.createAndRunInContext(`
11+
globalThis.foo = 'bar';
12+
foo;
13+
`);
14+
assert.strictEqual(result, 'bar');
15+
assert.strictEqual(globalThis.foo, undefined);
16+
17+
// Verifies that eval can be disabled in the addon-created context.
18+
assert.throws(() => binding.createAndRunInContext(`
19+
eval('"foo"');
20+
`), { name: 'EvalError' });
21+
22+
// Verifies that the addon-created context does not setup import loader.
23+
const p = binding.createAndRunInContext(`
24+
const p = import('node:fs');
25+
p;
26+
`);
27+
p.catch(common.mustCall((e) => {
28+
assert.throws(() => { throw e; }, { code: 'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING' });
29+
}));

0 commit comments

Comments
 (0)