-
Notifications
You must be signed in to change notification settings - Fork 29.6k
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
vm: add dynamic import support #22381
Conversation
e93c142
to
81cc223
Compare
Is this WIP or not so much? Either way, it needs a rebase… |
67b5f6f
to
580fb8a
Compare
@addaleax should be good to go now. also ping @nodejs/vm |
@devsnek Can you summarize the changes to contextify. |
This comment has been minimized.
This comment has been minimized.
580fb8a
to
51e65e9
Compare
doc/api/vm.md
Outdated
* `importModuleDynamically` {Function} Called during evaluation of this | ||
module when `import()` is called. | ||
* `specifier` {string} Specifier passed to `import()`. | ||
* `module`. {vm.SourceTextModule} This `vm.SourceTextModule` instance. |
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.
Nit: the period between `module`
and {vm.SourceTextModule}
seems not traditional in this place.
doc/api/vm.md
Outdated
* `importModuleDynamically` {Function} Called during evaluation of this | ||
module when `import()` is called. | ||
* `specifier` {string} Specifier passed to `import()`. | ||
* `script`. {vm.Script} This `vm.Script` instance. |
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.
Ditto.
51e65e9
to
b746fbd
Compare
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.
Great work.
src/env.h
Outdated
std::unordered_multimap<int, loader::ModuleWrap*> module_map; | ||
std::unordered_multimap<int, loader::ModuleWrap*> hash_to_module_map; | ||
std::unordered_multimap<uint32_t, loader::ModuleWrap*> id_to_module_map; | ||
std::unordered_multimap<uint32_t, contextify::ContextifyScript*> |
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.
This seems like a lot of maps to me - are you sure this is the simplest way to do this?
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.
it's a lot of maps. it's also the only way. We've talked to V8 about this before, but we're never going to be able to identify modules except by js primitive values so I use number -> wrap maps.
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.
Why is this a multimap? Shouldn't an ID uniquely identify a module? Furthermore, since the ID is always incremented one by one maybe some other data structure would be more appropriate than a hash table? A vector maybe. I guess you need to be able to erase elements. In that case a hash table is fine.
src/module_wrap.cc
Outdated
|
||
int type = options->Get(0).As<Number>()->Int32Value(); | ||
if (type == ScriptType::kScript) { | ||
uint32_t id = options->Get(1).As<Number>()->Uint32Value(); |
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.
This isn't my area at all, so can't really comment - but I'd be interested if you could explain what this ID is here?
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.
it comes from those counters I added to env
b746fbd
to
b331674
Compare
@@ -3,40 +3,44 @@ | |||
const { internalBinding } = require('internal/bootstrap/loaders'); | |||
const { | |||
setImportModuleDynamicallyCallback, | |||
setInitializeImportMetaObjectCallback | |||
setInitializeImportMetaObjectCallback, |
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.
Would you be able to revert these changes? I understand that trailing comma is your preferred style but they don't have to accompany everything else in this already large PR.
doc/api/vm.md
Outdated
* `importModuleDynamically` {Function} Called during evaluation of this | ||
module when `import()` is called. | ||
* `specifier` {string} Specifier passed to `import()`. | ||
* `module` {vm.SourceTextModule} This `vm.SourceTextModule` instance. |
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.
Could you be consistent with initalizeImportMeta
in the documentation style here? I.e., having the parameters be part of the description rather than as a sublist.
lib/internal/errors.js
Outdated
@@ -862,6 +862,8 @@ E('ERR_V8BREAKITERATOR', | |||
// This should probably be a `TypeError`. | |||
E('ERR_VALID_PERFORMANCE_ENTRY_TYPE', | |||
'At least one valid performance entry type is required', Error); | |||
E('ERR_VM_DYNAMIC_IMPORT_CALLBACK_MISSING', | |||
'A dynamic import callback was not specified.', Error); |
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.
TypeError?
src/node_contextify.cc
Outdated
Number::New(isolate, loader::ScriptType::kScript)); | ||
host_defined_options->Set(1, Number::New(isolate, contextify_script->id())); | ||
|
||
env->id_to_script_map.emplace(contextify_script->id(), contextify_script); |
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.
Perhaps this could be moved into the ContextifyScript
constructor?
src/module_wrap.cc
Outdated
@@ -157,7 +171,10 @@ void ModuleWrap::New(const FunctionCallbackInfo<Value>& args) { | |||
ModuleWrap* obj = new ModuleWrap(env, that, module, url); | |||
obj->context_.Reset(isolate, context); | |||
|
|||
env->module_map.emplace(module->GetIdentityHash(), obj); | |||
env->hash_to_module_map.emplace(module->GetIdentityHash(), obj); | |||
env->id_to_module_map.emplace(obj->id(), obj); |
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.
Ditto about moving this into the ModuleWrap
constructor.
importModuleDynamically(specifier, wrap) { | ||
assert.strictEqual(specifier, 'foo'); | ||
assert.strictEqual(wrap, s); | ||
return foo.namespace; |
This comment was marked as outdated.
This comment was marked as outdated.
Sorry, something went wrong.
src/module_wrap.cc
Outdated
|
||
Local<Value> object; | ||
|
||
int type = options->Get(0).As<Number>()->Int32Value(); |
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.
- You cannot assume that
options
always a) exists, b) has two items, c) both of which are integers, as embedders might be creating their own scripts and modules. - The
Int32Value
overload that does not take a context is deprecated and removed from the latest version of V8.
src/module_wrap.cc
Outdated
if (maybe_result.ToLocal(&result)) { | ||
return handle_scope.Escape(result.As<Promise>()); | ||
if (import_callback->Call( | ||
context, v8::Undefined(iso), 2, import_args).ToLocal(&result)) { |
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.
arraysize(import_args)
src/module_wrap.cc
Outdated
return handle_scope.Escape(result.As<Promise>()); | ||
if (import_callback->Call( | ||
context, v8::Undefined(iso), 2, import_args).ToLocal(&result)) { | ||
return result.As<Promise>(); |
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.
Add a CHECK(result->IsPromise());
here.
doc/api/vm.md
Outdated
@@ -171,6 +171,10 @@ const contextifiedSandbox = vm.createContext({ secret: 42 }); | |||
to initialize the `import.meta`. This function has the signature `(meta, | |||
module)`, where `meta` is the `import.meta` object in the `Module`, and | |||
`module` is this `vm.SourceTextModule` object. | |||
* `importModuleDynamically` {Function} Called during evaluation of this | |||
module when `import()` is called. |
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.
This gives me close to no information on how to use this option. I know when it's called and what information I'm given, but… what does it do? If I don't specify this, is import()
going to work? Is there a default importModuleDynamically
? What am I supposed to return? Examples?
6df2a36
to
5ad9d8a
Compare
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.
Was feeling some odd deja vu here that I'd said all my comments before... then remembered this is the reincarnation of #19717 :)
importModuleDynamically: common.mustCall((specifier, wrap) => { | ||
assert.strictEqual(specifier, 'foo'); | ||
assert.strictEqual(wrap, s); | ||
return foo.namespace; |
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.
Can we have some cases of invalid returns being tested here? What is the error?
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.
there is no limit on what you can return.
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.
This seems very dangerous (and I'm sure you'll agree!).
As an alternative could we not do something like:
- Only support a vm.Module instance from importModuleDynamically
- To delegate to
loader.import
have a special "loader" return value or indicator for this function.
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 don't really see any danger, could you fill me in on how you're approaching this?
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.
Well say I'm using this API for the first time - I will likely just return an object here and see that it works and make that the pattern I use in my code for dynamic imports. We shouldn't be encouraging a model that dynamic import is just "anything", but rather that it is another linking process in the graph. There's a big difference between exposing a hook that can return anything and a hook that goes through the same module logic again. So I'm just saying we should probably have it take vm.Module
instances as that is the module logic.
For example, error handling should be the same between environments - in this approach an error throw in dynamic import resolution isn't cached. That is a spec violation.
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.
As an alternative we could make the user return an object with a property pointing to the namespace object. This way we could typecheck the return value and avoid the whole thenable business until it is necessary.
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.
If we do check the type I would like it to only be an "is namespace" check, because returning a SourceTextModule instance doesn't guarantee that the SourceTextModule instance is in the correct state to be giving out namespaces, so i think its better to explicitly force the user to get that far. It's also worth mentioning that checking if a value is a module namespace is rather expensive because it's a native call.
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.
Yes a namespace check is fine.
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.
So the way the spec defines dynamic import is through a host hook that returns a Module record - https://tc39.github.io/proposal-dynamic-import/#sec-hostimportmoduledynamically.
This is done so that the error can then be taken from the module record itself.
As mentioned, I am very hesitant about the implementation of this function as breaks the spec on error handling here.
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.
To try and clarify some of the cases here:
- Having
import('x')
twice in the same source can return different modules each time it is called. This isn't explicitly a spec invariant, so could be seen as acceptable. - It is possible for
import('x')
to provide an error that does not correspond to an instantiation or evaluation error on a module record. - There are no guarantees that
import('x')
when resolving to the same module twice, will return the same cached error
With the namespace type check:
- It is possible for
import('x')
to not be able to resolve a valid module that is a thenable
Without the namespace type check:
- It is possible for
import('x')
to return any JavaScript binding without invoking any module loading algorithms at all.
If we mandated a return value type of "Module | "defaultLoader" () (not final API but the API suggestion, then the cases become:
- Once the promise has resolved to the "module" | default loader indicator string, we then get correct module errors and caching. Errors before this resolution that are not instantiation errors can still be spec-invariant, but we close this gap somewhat.
- It is possible to resolve thenable modules
- It is not possible to resolve an arbitrary JS binding without resolving through a Module
This comment has been minimized.
This comment has been minimized.
c742e31
to
ce01c43
Compare
src/node_contextify.h
Outdated
|
||
inline uint32_t id() { return id_; } | ||
|
||
SET_NO_MEMORY_INFO() |
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.
You have these macros called twice, that should be causing complilation failures
ce01c43
to
6cf2208
Compare
This comment has been minimized.
This comment has been minimized.
6cf2208
to
948fcb4
Compare
rebuild of containered https://ci.nodejs.org/job/node-test-commit-linux-containered/7619/ and https://ci.nodejs.org/job/node-test-commit-linux-containered/7620/ because checking boxes is hard |
PR-URL: nodejs#22381 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Guy Bedford <guybedford@gmail.com> Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
948fcb4
to
4c37df7
Compare
landed in 4c37df7 |
Should this be backported to |
PR-URL: #22381 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Guy Bedford <guybedford@gmail.com> Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
PR-URL: nodejs#22381 Reviewed-By: James M Snell <jasnell@gmail.com> Reviewed-By: Guy Bedford <guybedford@gmail.com> Reviewed-By: Tiancheng "Timothy" Gu <timothygu99@gmail.com>
importModuleDynamically
callback option tovm.SourceTextModule
andvm.Script
constructors.node_contextify.h
andnode_contextify.cc
Closes #19363
Refs #19570
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passes