-
-
Notifications
You must be signed in to change notification settings - Fork 33k
vm: add experimental NodeRealm implementation #47855
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
Changes from 5 commits
5aebfda
a624c7e
b04d52c
156fad5
8ff9d0a
1050cb2
1b5978b
4211750
8929ecd
7f56539
db8bf72
62e534b
bb0a04a
75618e6
1b8059f
6184855
0ffaba0
aa46778
c3f4321
39e58b3
8474e51
8240d61
f371400
9f1d1a5
57409e6
db0c89b
3988738
db395ac
7ba9bbd
4209518
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 |
---|---|---|
|
@@ -1575,7 +1575,8 @@ are not controllable through the timeout either. | |
|
||
### Class: `NodeRealm` | ||
|
||
> Stability: 1 - Experimental. Use `--experimental-noderealm` CLI flag to enable this feature. | ||
> Stability: 1 - Experimental. Use `--experimental-node-realm` CLI flag to | ||
> enable this feature. | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
|
@@ -1584,12 +1585,17 @@ added: REPLACEME | |
* Extends: {EventEmitter} | ||
|
||
A `NodeRealm` is effectively a Node.js environment that runs within the | ||
same thread. | ||
same thread. It similar to a [ShadowRealm][], but with a few main differences: | ||
|
||
* `NodeRealm` allows to load both commonjs and ESM modules. | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* Full interoperability between the host realm and the `NodeRealm` instance | ||
is allowed | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
* There is a deliberate `stop()` function. | ||
|
||
```mjs | ||
import { NodeRealm } from 'node:vm'; | ||
const noderealm = new NodeRealm(); | ||
const myAsyncFunction = noderealm.createImport(import.meta.url)('my-module'); | ||
const nodeRealm = new NodeRealm(); | ||
const myAsyncFunction = nodeRealm.createImport(import.meta.url)('my-module'); | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
console.log(await myAsyncFunction()); | ||
``` | ||
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 do think the docs should clarify the difference between this and a 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 would also like to understand the differences (and similarities) between this and a worker. Because they look very similar. For example, does a realm have an event loop? Does it share globals? (I'm assuming yes and no?) |
||
|
||
|
@@ -1599,7 +1605,7 @@ console.log(await myAsyncFunction()); | |
added: REPLACEME | ||
--> | ||
|
||
#### `noderealm.stop()` | ||
#### `nodeRealm.stop()` | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
|
@@ -1614,18 +1620,18 @@ This method returns a promise that will be resolved when all resources | |
associated with this Node.js instance are released. This promise resolves on | ||
the event loop of the _outer_ Node.js instance. | ||
|
||
#### `noderealm.createImport(filename)` | ||
#### `nodeRealm.createImport(filename)` | ||
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. Can we create the NodeRealm with a specifier? interface NodeRealm {
constructor(specifier: string);
import(specifier, importAssertions): ModuleNamespace;
} In this way, we can get rid of the higher order function import { NodeRealm } from 'node:vm';
const nodeRealm = new NodeRealm(import.meta.url);
const { myAsyncFunction } = await nodeRealm.import('my-module');
console.log(await myAsyncFunction());
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
<!-- YAML | ||
added: REPLACEME | ||
--> | ||
|
||
* `filename` {string} | ||
* `specifier` {string} A module specifier like './file.js' or 'my-package' | ||
|
||
Create a function that can be used for loading | ||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||
modules inside the inner Node.js instance. | ||
|
||
#### `noderealm.globalThis` | ||
#### `nodeRealm.globalThis` | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
|
@@ -1635,7 +1641,7 @@ added: REPLACEME | |
|
||
Returns a reference to the global object of the inner Node.js instance. | ||
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. It should be clarified whether this value is mutable. e.g. is it possible to |
||
|
||
#### `noderealm.process` | ||
#### `nodeRealm.process` | ||
|
||
<!-- YAML | ||
added: REPLACEME | ||
|
@@ -1671,3 +1677,4 @@ Returns a reference to the `process` object of the inner Node.js instance. | |
[global object]: https://es5.github.io/#x15.1 | ||
[indirect `eval()` call]: https://es5.github.io/#x10.4.2 | ||
[origin]: https://developer.mozilla.org/en-US/docs/Glossary/Origin | ||
[ShadowRealm]: https://github.com/tc39/proposal-shadowrealm |
Original file line number | Diff line number | Diff line change | ||
---|---|---|---|---|
|
@@ -21,8 +21,8 @@ | |||
|
||||
#include "node_contextify.h" | ||||
|
||||
#include "base_object-inl.h" | ||||
#include "async_wrap-inl.h" | ||||
#include "base_object-inl.h" | ||||
#include "memory_tracker-inl.h" | ||||
#include "module_wrap.h" | ||||
#include "node_context_data.h" | ||||
|
@@ -1398,7 +1398,7 @@ void MicrotaskQueueWrap::RegisterExternalReferences( | |||
Local<FunctionTemplate> NodeRealm::GetConstructorTemplate( | ||||
IsolateData* isolate_data) { | ||||
Local<FunctionTemplate> tmpl = | ||||
isolate_data->noderealm_constructor_template(); | ||||
isolate_data->node_realm_constructor_template(); | ||||
if (tmpl.IsEmpty()) { | ||||
Isolate* isolate = isolate_data->isolate(); | ||||
tmpl = NewFunctionTemplate(isolate, New); | ||||
|
@@ -1412,13 +1412,13 @@ Local<FunctionTemplate> NodeRealm::GetConstructorTemplate( | |||
SetProtoMethod(isolate, tmpl, "tryCloseAllHandles", TryCloseAllHandles); | ||||
SetProtoMethod(isolate, tmpl, "internalRequire", InternalRequire); | ||||
|
||||
isolate_data->set_noderealm_constructor_template(tmpl); | ||||
isolate_data->set_node_realm_constructor_template(tmpl); | ||||
} | ||||
return tmpl; | ||||
} | ||||
|
||||
void NodeRealm::CreatePerIsolateProperties(IsolateData* isolate_data, | ||||
v8::Local<v8::ObjectTemplate> target) { | ||||
void NodeRealm::CreatePerIsolateProperties( | ||||
IsolateData* isolate_data, v8::Local<v8::ObjectTemplate> target) { | ||||
SetConstructorFunction(isolate_data->isolate(), | ||||
target, | ||||
"NodeRealm", | ||||
|
@@ -1437,8 +1437,7 @@ void NodeRealm::RegisterExternalReferences( | |||
registry->Register(InternalRequire); | ||||
} | ||||
|
||||
NodeRealm::NodeRealmScope::NodeRealmScope( | ||||
NodeRealm* w) | ||||
NodeRealm::NodeRealmScope::NodeRealmScope(NodeRealm* w) | ||||
: EscapableHandleScope(w->isolate_), | ||||
Scope(w->context()), | ||||
Isolate::SafeForTerminationScope(w->isolate_), | ||||
|
@@ -1464,8 +1463,7 @@ NodeRealm::NodeRealm(Environment* env, Local<Object> object) | |||
outer_context_.Reset(env->isolate(), outer_context); | ||||
} | ||||
|
||||
NodeRealm* NodeRealm::Unwrap( | ||||
const FunctionCallbackInfo<Value>& args) { | ||||
NodeRealm* NodeRealm::Unwrap(const FunctionCallbackInfo<Value>& args) { | ||||
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. Should NodeRealm inherit from BaseObject? This seems pretty similar to BaseObject's work. 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. Yes. Only reason I didn't do this in synchronous-worker is because it was built outside of Node.js core. |
||||
Local<Value> value = args.This(); | ||||
if (!value->IsObject() || value.As<Object>()->InternalFieldCount() < 1) { | ||||
THROW_ERR_INVALID_THIS(Environment::GetCurrent(args.GetIsolate())); | ||||
|
@@ -1486,8 +1484,7 @@ void NodeRealm::Start(const FunctionCallbackInfo<Value>& args) { | |||
self->Start(); | ||||
} | ||||
|
||||
void NodeRealm::TryCloseAllHandles( | ||||
const FunctionCallbackInfo<Value>& args) { | ||||
void NodeRealm::TryCloseAllHandles(const FunctionCallbackInfo<Value>& args) { | ||||
auto count = 0; | ||||
NodeRealm* self = Unwrap(args); | ||||
mcollina marked this conversation as resolved.
Show resolved
Hide resolved
|
||||
if (self == nullptr) return; | ||||
|
@@ -1496,11 +1493,10 @@ void NodeRealm::TryCloseAllHandles( | |||
args.GetReturnValue().Set(v8::Number::New(self->isolate_, count)); | ||||
} | ||||
|
||||
void NodeRealm::InternalRequire( | ||||
const FunctionCallbackInfo<Value>& args) { | ||||
void NodeRealm::InternalRequire(const FunctionCallbackInfo<Value>& args) { | ||||
NodeRealm* self = Unwrap(args); | ||||
Local<Function> require = Realm::GetCurrent( | ||||
self->context())->builtin_module_require(); | ||||
Local<Function> require = | ||||
Realm::GetCurrent(self->context())->builtin_module_require(); | ||||
args.GetReturnValue().Set(require); | ||||
} | ||||
|
||||
|
@@ -1540,7 +1536,7 @@ void NodeRealm::Start() { | |||
assert(loop != nullptr); | ||||
|
||||
MicrotaskQueue* microtask_queue = | ||||
outer_context_.Get(isolate_)->GetMicrotaskQueue(); | ||||
outer_context_.Get(isolate_)->GetMicrotaskQueue(); | ||||
|
||||
Local<Context> context = Context::New( | ||||
isolate_, | ||||
|
@@ -1563,8 +1559,8 @@ void NodeRealm::Start() { | |||
GetArrayBufferAllocator(GetEnvironmentIsolateData(outer_env))); | ||||
assert(isolate_data_ != nullptr); | ||||
ThreadId thread_id = AllocateEnvironmentThreadId(); | ||||
auto inspector_parent_handle = GetInspectorParentHandle( | ||||
outer_env, thread_id, "file:///noderealm.js"); | ||||
auto inspector_parent_handle = | ||||
GetInspectorParentHandle(outer_env, thread_id, "file:///node_realm.js"); | ||||
env_ = CreateEnvironment(isolate_data_, | ||||
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. This doesn't seem to be a good idea - I think so far we've been assuming in the code base that there is a 1:1 relationship between the environment and the isolate, things can be subtly broken once we have n:1. It would be cleaner if we follow the Realm approach and just split out states in the Environment that are unique to each context/realm to a subclass of 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.
Quite a few of the crashes I've been fighting with this approach are due to that. I'm using the reference to The other question I have if we dith the 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. You can just move the handles into the Realm instead. Basically, just move things that should belong to individual realms instead of being shared across them to the realm, and do the setup/cleanup on a per-realm basis, which is what we've been trying to do with the ShadowRealm integration. 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.
How would you do that? As an example Line 110 in c542d3a
Should I try to move that list from the Env to the Realm? 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. Yes, just make that 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. Yeah, tracking handle wraps and request wraps by realms is the plan for shadow realm integration. It's not the current focus yet. However, moving the list from the Environment to the Realm breaks the postmortem diagnostics since tools like llnode are built on top of the An option can be tracking the handle wraps and request wraps by both the Env and its creation Realm. 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 we don't need to worry much about the postmortem diagnostics data, we only need to leave the offset of the queues within Realms and the tools would then figure out how to adapt to newer versions of Node.js. It's never guaranteed that we'd never change the layout of our internals, only that when we do, we still leave some information in the binary (the offsets) for these tools to figure out how to extract information from a core dump / process memory. 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. My understanding is that we won't be able to have a May we land this PR and work to remove 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.
That that's not the case is precisely the difference between 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 don't think in practice we have really been writing the code that way. There are some places where we configure V8 isolate hooks (e.g. the ones in |
||||
context, | ||||
{}, | ||||
|
Uh oh!
There was an error while loading. Please reload this page.