-
-
Notifications
You must be signed in to change notification settings - Fork 35.1k
src: expose node::MakeContextify to make a node managed vm context
#62322
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -571,6 +571,44 @@ 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_; | ||
| 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 `context_object`. 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> context_object, | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think unless you are specifically after those weird quirky interceptor behaviors e.g. #31808, if we want a Node.js-managed context API, the most natural thing is to have
Member
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If the |
||
| const ContextifyOptions& options); | ||
| NODE_EXTERN v8::MaybeLocal<v8::Context> GetContextified( | ||
| Environment* env, v8::Local<v8::Object> context_object); | ||
|
|
||
| // 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. | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,79 @@ | ||
| #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 MakeContext(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; | ||
| } | ||
|
|
||
| // Return the global proxy object. | ||
| args.GetReturnValue().Set(vm_context->Global()); | ||
| } | ||
|
|
||
| void RunInContext(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); | ||
|
|
||
| Local<Object> sandbox_obj = args[0].As<Object>(); | ||
| v8::MaybeLocal<Context> maybe_context = | ||
| node::GetContextified(env, sandbox_obj); | ||
| v8::Local<Context> vm_context; | ||
| if (!maybe_context.ToLocal(&vm_context)) { | ||
| return; | ||
| } | ||
| Context::Scope context_scope(vm_context); | ||
|
|
||
| if (args.Length() < 2 || !args[1]->IsString()) { | ||
| return; | ||
| } | ||
| Local<String> source = args[1].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, "makeContext", MakeContext); | ||
| NODE_SET_METHOD(exports, "runInContext", RunInContext); | ||
| } | ||
|
|
||
| } // anonymous namespace | ||
|
|
||
| NODE_MODULE_CONTEXT_AWARE(NODE_GYP_MODULE_NAME, Initialize) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| 'targets': [ | ||
| { | ||
| 'target_name': 'binding', | ||
| 'sources': ['binding.cc'], | ||
| 'includes': ['../common.gypi'], | ||
| }, | ||
| ] | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,51 @@ | ||
| '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 ctx = binding.makeContext(); | ||
| const result = binding.runInContext(ctx, '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'); | ||
| const ctx = binding.makeContext(); | ||
| binding.runInContext(ctx, 'debugger'); | ||
| await pausedPromise; | ||
|
|
||
| session.post('Debugger.resume'); | ||
| } | ||
| })().then(common.mustCall()); |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,43 @@ | ||
| 'use strict'; | ||
|
|
||
| const common = require('../../common'); | ||
| const assert = require('assert'); | ||
| const vm = require('vm'); | ||
|
|
||
| const binding = require(`./build/${common.buildType}/binding`); | ||
|
|
||
| // This verifies that the addon-created context has an independent | ||
| // global object. | ||
| { | ||
| const ctx = binding.makeContext(); | ||
| const result = binding.runInContext(ctx, ` | ||
| 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.runInContext(ctx, ` | ||
| eval('"foo"'); | ||
| `), { name: 'EvalError' }); | ||
|
|
||
| // Verifies that the addon-created context does not setup import loader. | ||
| const p = binding.runInContext(ctx, ` | ||
| const p = import('node:fs'); | ||
| p; | ||
| `); | ||
| p.catch(common.mustCall((e) => { | ||
| assert.throws(() => { throw e; }, { code: 'ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING' }); | ||
| })); | ||
| } | ||
|
|
||
|
|
||
| // Verifies that the addon can unwrap the context from a vm context | ||
| { | ||
| const ctx = vm.createContext(vm.constants.DONT_CONTEXTIFY); | ||
| vm.runInContext('globalThis.foo = {}', ctx); | ||
| const result = binding.runInContext(ctx, 'globalThis.foo'); | ||
| // The return value identities should be equal. | ||
| assert.strictEqual(result, vm.runInContext('globalThis.foo', ctx)); | ||
| } |
There was a problem hiding this comment.
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