Skip to content
Open
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
48 changes: 48 additions & 0 deletions src/api/environment.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
#include "node.h"
#include "node_builtins.h"
#include "node_context_data.h"
#include "node_contextify.h"
#include "node_debug.h"
#include "node_errors.h"
#include "node_exit_code.h"
Expand Down Expand Up @@ -1057,6 +1058,53 @@ Maybe<bool> InitializeContext(Local<Context> context) {
return Just(true);
}

ContextifyOptions::ContextifyOptions(Local<String> name,
Local<String> origin,
bool allow_code_gen_strings,
bool allow_code_gen_wasm,
MicrotaskMode microtask_mode)
: name_(name),
origin_(origin),
allow_code_gen_strings_(allow_code_gen_strings),
allow_code_gen_wasm_(allow_code_gen_wasm),
microtask_mode_(microtask_mode) {}

MaybeLocal<Context> MakeContextify(Environment* env,
Local<Object> contextObject,
const ContextifyOptions& options) {
Isolate* isolate = env->isolate();
std::unique_ptr<v8::MicrotaskQueue> microtask_queue;
if (options.microtask_mode() ==
ContextifyOptions::MicrotaskMode::kAfterEvaluate) {
microtask_queue = v8::MicrotaskQueue::New(env->isolate(),
v8::MicrotasksPolicy::kExplicit);
}

contextify::ContextOptions ctxOptions{
.name = options.name(),
.origin = options.origin(),
.allow_code_gen_strings =
Boolean::New(isolate, options.allow_code_gen_strings()),
.allow_code_gen_wasm =
Boolean::New(isolate, options.allow_code_gen_wasm()),
.own_microtask_queue = std::move(microtask_queue),
.host_defined_options_id = env->vm_dynamic_import_no_callback(),
.vanilla = contextObject.IsEmpty(),
};

TryCatchScope try_catch(env);
contextify::ContextifyContext* context_ptr =
contextify::ContextifyContext::New(env, contextObject, &ctxOptions);

if (try_catch.HasCaught()) {
if (!try_catch.HasTerminated()) try_catch.ReThrow();
// Allocation failure, maximum call stack size reached, termination, etc.
return {};
}

return context_ptr->context();
}

uv_loop_t* GetCurrentEventLoop(Isolate* isolate) {
HandleScope handle_scope(isolate);
Local<Context> context = isolate->GetCurrentContext();
Expand Down
36 changes: 36 additions & 0 deletions src/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -571,6 +571,42 @@ NODE_EXTERN v8::Local<v8::Context> NewContext(
// Return value indicates success of operation
NODE_EXTERN v8::Maybe<bool> InitializeContext(v8::Local<v8::Context> context);

class ContextifyOptions {
public:
enum class MicrotaskMode {
kDefault,
kAfterEvaluate,
};

ContextifyOptions(v8::Local<v8::String> name,
v8::Local<v8::String> origin,
bool allow_code_gen_strings,
bool allow_code_gen_wasm,
MicrotaskMode microtask_mode);

v8::Local<v8::String> name() const { return name_; }
v8::Local<v8::String> origin() const { return origin_; }
bool allow_code_gen_strings() const { return allow_code_gen_strings_; }
bool allow_code_gen_wasm() const { return allow_code_gen_wasm_; }
MicrotaskMode microtask_mode() const { return microtask_mode_; }

private:
v8::Local<v8::String> name_;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd suggest adding a version field on this struct for easier ABI compatibility if we want to make changes at some point

v8::Local<v8::String> origin_;
bool allow_code_gen_strings_;
bool allow_code_gen_wasm_;
MicrotaskMode microtask_mode_;
};

// Create a Node.js managed v8::Context with the `contextObject`. If the
// `contextObject` is an empty handle, the v8::Context is created without
// wrapping its global object with an object in a Node.js-specific manner.
// The created context is supported in Node.js inspector.
NODE_EXTERN v8::MaybeLocal<v8::Context> MakeContextify(
Environment* env,
v8::Local<v8::Object> contextObject,
const ContextifyOptions& options);

// If `platform` is passed, it will be used to register new Worker instances.
// It can be `nullptr`, in which case creating new Workers inside of
// Environments that use this `IsolateData` will not work.
Expand Down
3 changes: 2 additions & 1 deletion src/node_contextify.h
Original file line number Diff line number Diff line change
Expand Up @@ -131,10 +131,11 @@ class ContextifyContext final : CPPGC_MIXIN(ContextifyContext) {

static void InitializeGlobalTemplates(IsolateData* isolate_data);

private:
static ContextifyContext* New(Environment* env,
v8::Local<v8::Object> sandbox_obj,
ContextOptions* options);

private:
// Initialize a context created from CreateV8Context()
static ContextifyContext* New(v8::Local<v8::Context> ctx,
Environment* env,
Expand Down
59 changes: 59 additions & 0 deletions test/addons/new-context-inspector/binding.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
#include <node.h>
#include <v8.h>

namespace {

using v8::Context;
using v8::FunctionCallbackInfo;
using v8::HandleScope;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::Script;
using v8::String;
using v8::Value;

void CreateAndRunInContext(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = Isolate::GetCurrent();
HandleScope handle_scope(isolate);
Local<Context> context = isolate->GetCurrentContext();
node::Environment* env = node::GetCurrentEnvironment(context);
assert(env);

node::ContextifyOptions options(
String::NewFromUtf8Literal(isolate, "Addon Context"),
String::NewFromUtf8Literal(isolate, "addon://about"),
false,
false,
node::ContextifyOptions::MicrotaskMode::kDefault);
// Create a new context with Node.js-specific vm setup.
v8::MaybeLocal<Context> maybe_context =
node::MakeContextify(env, {}, options);
v8::Local<Context> vm_context;
if (!maybe_context.ToLocal(&vm_context)) {
return;
}
Context::Scope context_scope(vm_context);

if (args.Length() == 0 || !args[0]->IsString()) {
return;
}
Local<String> source = args[0].As<String>();
Local<Script> script;
Local<Value> result;

if (Script::Compile(vm_context, source).ToLocal(&script) &&
script->Run(vm_context).ToLocal(&result)) {
args.GetReturnValue().Set(result);
}
}

void Initialize(Local<Object> exports,
Local<Value> module,
Local<Context> context) {
NODE_SET_METHOD(exports, "createAndRunInContext", CreateAndRunInContext);
}

} // anonymous namespace

NODE_MODULE_CONTEXT_AWARE(NODE_GYP_MODULE_NAME, Initialize)
9 changes: 9 additions & 0 deletions test/addons/new-context-inspector/binding.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
{
'targets': [
{
'target_name': 'binding',
'sources': ['binding.cc'],
'includes': ['../common.gypi'],
},
]
}
49 changes: 49 additions & 0 deletions test/addons/new-context-inspector/test-inspector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
'use strict';

const common = require('../../common');
common.skipIfInspectorDisabled();

const assert = require('node:assert');
const { once } = require('node:events');
const { Session } = require('node:inspector');

const binding = require(`./build/${common.buildType}/binding`);

const session = new Session();
session.connect();

(async function() {
const mainContextPromise =
once(session, 'Runtime.executionContextCreated');
session.post('Runtime.enable', assert.ifError);
await mainContextPromise;

// Addon-created context should be reported to the inspector.
{
const addonContextPromise =
once(session, 'Runtime.executionContextCreated');

const result = binding.createAndRunInContext('1 + 1');
assert.strictEqual(result, 2);

const { 0: contextCreated } = await addonContextPromise;
const { name, origin, auxData } = contextCreated.params.context;
assert.strictEqual(name, 'Addon Context',
JSON.stringify(contextCreated));
assert.strictEqual(origin, 'addon://about',
JSON.stringify(contextCreated));
assert.strictEqual(auxData.isDefault, false,
JSON.stringify(contextCreated));
}

// `debugger` statement should pause in addon-created context.
{
session.post('Debugger.enable', assert.ifError);

const pausedPromise = once(session, 'Debugger.paused');
binding.createAndRunInContext('debugger');
await pausedPromise;

session.post('Debugger.resume');
}
})().then(common.mustCall());
29 changes: 29 additions & 0 deletions test/addons/new-context-inspector/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
'use strict';

const common = require('../../common');
const assert = require('assert');

const binding = require(`./build/${common.buildType}/binding`);

// This verifies that the addon-created context has an independent
// global object.
const result = binding.createAndRunInContext(`
globalThis.foo = 'bar';
foo;
`);
assert.strictEqual(result, 'bar');
assert.strictEqual(globalThis.foo, undefined);

// Verifies that eval can be disabled in the addon-created context.
assert.throws(() => binding.createAndRunInContext(`
eval('"foo"');
`), { name: 'EvalError' });

// Verifies that the addon-created context does not setup import loader.
const p = binding.createAndRunInContext(`
const p = import('node:fs');
p;
`);
p.catch(common.mustCall((e) => {
assert.throws(() => { throw e; }, { code: 'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING' });
}));
Loading