Skip to content
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

Merged
merged 1 commit into from
Oct 6, 2018

Conversation

devsnek
Copy link
Member

@devsnek devsnek commented Aug 17, 2018

  • adds importModuleDynamically callback option to vm.SourceTextModule and vm.Script constructors.
  • splits contextify::ContextifyScript into node_contextify.h and node_contextify.cc

Closes #19363
Refs #19570

Checklist
  • make -j4 test (UNIX), or vcbuild test (Windows) passes
  • tests and/or benchmarks are included
  • documentation is changed or added
  • commit message follows commit guidelines

@devsnek devsnek added vm Issues and PRs related to the vm subsystem. esm Issues and PRs related to the ECMAScript Modules implementation. labels Aug 17, 2018
@devsnek devsnek requested review from bmeck and jkrems August 17, 2018 23:14
@nodejs-github-bot nodejs-github-bot added the lib / src Issues and PRs related to general changes in the lib or src directory. label Aug 17, 2018
@devsnek devsnek force-pushed the feature/vm-dynamic-import branch 2 times, most recently from e93c142 to 81cc223 Compare August 19, 2018 01:48
@addaleax
Copy link
Member

better docs and tests coming as soon(tm)

Is this WIP or not so much? Either way, it needs a rebase…

@devsnek devsnek force-pushed the feature/vm-dynamic-import branch 2 times, most recently from 67b5f6f to 580fb8a Compare August 24, 2018 00:12
@devsnek
Copy link
Member Author

devsnek commented Aug 24, 2018

@addaleax should be good to go now.

also ping @nodejs/vm

@jdalton
Copy link
Member

jdalton commented Aug 24, 2018

@devsnek Can you summarize the changes to contextify.

@vsemozhetbyt

This comment has been minimized.

@devsnek
Copy link
Member Author

devsnek commented Aug 24, 2018

@vsemozhetbyt did that 👍

CI https://ci.nodejs.org/job/node-test-pull-request/16737/

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.
Copy link
Contributor

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.
Copy link
Contributor

Choose a reason for hiding this comment

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

Ditto.

@devsnek
Copy link
Member Author

devsnek commented Aug 25, 2018

Copy link
Contributor

@guybedford guybedford left a comment

Choose a reason for hiding this comment

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

Great work.

lib/internal/modules/esm/translators.js Show resolved Hide resolved
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*>
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Member

@TimothyGu TimothyGu Aug 25, 2018

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.


int type = options->Get(0).As<Number>()->Int32Value();
if (type == ScriptType::kScript) {
uint32_t id = options->Get(1).As<Number>()->Uint32Value();
Copy link
Contributor

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?

Copy link
Member Author

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

@@ -3,40 +3,44 @@
const { internalBinding } = require('internal/bootstrap/loaders');
const {
setImportModuleDynamicallyCallback,
setInitializeImportMetaObjectCallback
setInitializeImportMetaObjectCallback,
Copy link
Member

@TimothyGu TimothyGu Aug 25, 2018

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.
Copy link
Member

@TimothyGu TimothyGu Aug 25, 2018

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.

@@ -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);
Copy link
Member

Choose a reason for hiding this comment

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

TypeError?

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);
Copy link
Member

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?

@@ -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);
Copy link
Member

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.


Local<Value> object;

int type = options->Get(0).As<Number>()->Int32Value();
Copy link
Member

Choose a reason for hiding this comment

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

  1. 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.
  2. The Int32Value overload that does not take a context is deprecated and removed from the latest version of V8.

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)) {
Copy link
Member

Choose a reason for hiding this comment

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

arraysize(import_args)

return handle_scope.Escape(result.As<Promise>());
if (import_callback->Call(
context, v8::Undefined(iso), 2, import_args).ToLocal(&result)) {
return result.As<Promise>();
Copy link
Member

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.
Copy link
Member

@TimothyGu TimothyGu Aug 25, 2018

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?

@devsnek devsnek force-pushed the feature/vm-dynamic-import branch 3 times, most recently from 6df2a36 to 5ad9d8a Compare August 25, 2018 23:46
Copy link
Contributor

@guybedford guybedford left a 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;
Copy link
Contributor

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?

Copy link
Member Author

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.

Copy link
Contributor

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.

Copy link
Member Author

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?

Copy link
Contributor

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.

Copy link
Member

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.

Copy link
Member Author

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.

Copy link
Member

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.

Copy link
Contributor

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.

Copy link
Contributor

@guybedford guybedford Aug 27, 2018

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

@devsnek

This comment has been minimized.

@devsnek devsnek force-pushed the feature/vm-dynamic-import branch 2 times, most recently from c742e31 to ce01c43 Compare October 4, 2018 18:47

inline uint32_t id() { return id_; }

SET_NO_MEMORY_INFO()
Copy link
Member

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

@devsnek

This comment has been minimized.

@devsnek
Copy link
Member Author

devsnek commented Oct 5, 2018

@devsnek
Copy link
Member Author

devsnek commented Oct 6, 2018

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>
@devsnek devsnek merged commit 4c37df7 into nodejs:master Oct 6, 2018
@devsnek
Copy link
Member Author

devsnek commented Oct 6, 2018

landed in 4c37df7

@devsnek devsnek deleted the feature/vm-dynamic-import branch October 6, 2018 22:34
@targos
Copy link
Member

targos commented Oct 7, 2018

Should this be backported to v10.x-staging? If yes please follow the guide and raise a backport PR, if not let me know or add the dont-land-on label.

jasnell pushed a commit that referenced this pull request Oct 17, 2018
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>
joyeecheung pushed a commit to joyeecheung/node that referenced this pull request Jan 9, 2019
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>
BethGriggs pushed a commit that referenced this pull request Feb 5, 2019
Backport-PR-URL: #25421
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>
@BethGriggs BethGriggs mentioned this pull request Feb 12, 2019
rvagg pushed a commit that referenced this pull request Feb 28, 2019
Backport-PR-URL: #25421
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
esm Issues and PRs related to the ECMAScript Modules implementation. lib / src Issues and PRs related to general changes in the lib or src directory. vm Issues and PRs related to the vm subsystem.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

Call linker when dynamically importing inside a vm.Module