-
Notifications
You must be signed in to change notification settings - Fork 30k
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
addons: allow multiple requires of the same addon #19731
Conversation
This commit also fixes a long-standing |
df0ab55
to
6099f0a
Compare
@nodejs/collaborators This has been open for 3 days but hasn't received any reviews/comments. |
I flagged it as semver-major for any potential breakage. |
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.
LGTM with a nit.
test/addons/not-a-binding/test.js
Outdated
@@ -2,5 +2,5 @@ | |||
const common = require('../../common'); | |||
const assert = require('assert'); | |||
|
|||
const re = /^Error: Module did not self-register\.$/; | |||
const re = /^Error: Module did not self-register/; |
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 you put the full error message here?
6099f0a
to
5fc87d1
Compare
@mcollina I updated the test. |
I think this needs a test for n-api modules as well as the functionality for both n-api and n-api modules needs to be on par. |
5fc87d1
to
8a5d3cc
Compare
@mhdawson I added the NAPI test, and validated that this change makes it pass. |
I have currently only tested the changes on macOS. Is there a way to trigger the CI system to build these changes? I was not able to find anything on https://ci.nodejs.org/job/node-test-pull-request/ that would let me trigger a job (nor any documentation). |
Replace the global state tracking a single pending addon that is to be loaded with a map from addon name to 'node_module*'. This allows multiple, independent 'node::Environment's to load the same addon. Add a test to validate this new behavior by clearing the require cache and reloading the addon. NOTE: This will break any user that is not following the documentation and requiring an addon via a different name than that passed as the first argument to `NODE_MODULE(...)`.
8a5d3cc
to
62497a5
Compare
I don't get it. It's already possible to |
I have created #19875 to test the case where a module is loaded twice. |
@gabrielschulhof the fix from @bnoordhuis in 3828fc6 works very well if you use the well-known symbol approach to define an add-on, but does not work so well if you use to other well-documented method of using the macro to define an add-on. My PR fixes the latter case. The former case is not a problem since the change in 3828fc6. |
This is especially problematic since the Electron project introduced support for the If each of these environments are truly independent, they should be allowed to load a Node add-on built using either the standard macro or using the well-known symbol. |
@frutiger OK, right, if the well-known symbol is absent, you have to maintain a list of already-loaded modules keyed by their requested path. Thanks for clarifying! |
} else { | ||
modpending = mp; | ||
} else if (mods.count(mp->nm_modname) == 0) { | ||
mods[mp->nm_modname] = mp; |
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 think we need a stronger key because this also breaks those projects which use two different versions of the same module. So, like,
project
|
+-dep1
| |
| +-native-addon1(@2.2.0)
|
+-native-addon1(@1.2.0)
The distinction between the two modules with identical nm_modname
is the path from which they are loaded. So we should save the absolute path of the module we are about to load to a global variable declared next to modpending
right before calling dlib.Open()
, and then use that path as the key here.
So, like
static node_module* modpending;
+std::string modpending_filename;
and then
mods[modpending_filename.c_str()] = mp;
in node_module_register()
, and
+modpending_filename = std::string(*filename);
bool is_opened = dlib.Open();
in DLOpen()
.
In general, I have reservations about attempting to duplicate what the library loader is doing, because that code has very likely been refined to a very high degree over the course of the decades.
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 are absolutely right, the basename is not unambiguous enough.
I can change it as you suggest - in fact we don't even need to save the pending filename, we can just he the pending node_module *
as we do today and use its file name as the key in the std::unordered_map
.
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.
@frutiger we must also ensure that two different file names which refer to the same file in reality are treated correctly. I'm not sure to what extent the JS side of require()
takes care of resolving files in this way.
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.
@frutiger it might be worth also taking a look at the JS side, and maybe adding some more resolution steps to the portion of the code path following the decision to load a native module.
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.
@frutiger I guess we should at least follow symlinks.
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'm not actually sure just following symlinks is enough, nor do I think we would be able to do a good job of maintaining compatibility of this logic with the loader logic in JS. Perhaps we could hash the contents of the file itself?
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.
Ideally, the JS loader logic would cause the resulting call to dlopen()
to always return a new module, and run the constructors. However, this means that we will have reproduced dlopen()
's decision as to whether to load a library or increment the refcount of an existing one. I'm pretty sure we haven't done that.
The decision made by dlopen()
as to whether to load a new library, thereby running its constructors, or incrementing the reference count of an existing library, thereby not running its constructors, is what we're trying to replicate here. The code for that decision is very old, very well-vetted, and may even be platform-dependent. This is why I said I was reluctant to attempt to reproduce that code.
In an ideal world we'd have dlreopen()
or the RTLD_REOPEN
flag which would do exactly what we need. But alas.
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 guess @bnoordhuis' modification whereby it falls back to looking for a well-known symbol is a way to kick-start the addon if it fails to register. But, like you said, this won't work for existing modules. sigh
triggering CI as requested above to get broader platform coverage: https://ci.nodejs.org/job/node-test-pull-request/14157/ |
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 appreciate you're taking this on but this approach is not safe. Many add-ons do global initialization in their init hook. Doing that twice will result in... interesting... effects.
Red X to counter @mcollina's LGTM. This can't land as-is.
For our use case, I can take the well-known symbol approach, so I appreciate all the feedback and I don't actually need the change proposed here. I am curious though @bnoordhuis: if an add-on does global initialization on load, it clearly cannot work across multiple contexts, and so is it worth attempting to support them for the multi-use case? |
@frutiger you can attach state during You could also create a global map keyed on the |
... so any and all addons that will want/claim to support multi-context will have to be modified to store their state in such a fashion. |
We will be personally changing our add-ons to use the well-known symbol instead of using a load-time registration function. Abandoning this PR as a result. |
Replace the global state tracking a single pending addon that is to be
loaded with a map from addon name to
node_module*
. This allowsmultiple, independent
node::Environment
s to load the same addon.Add a test to validate this new behavior by clearing the require cache
and reloading the addon.
NOTE: This will break any user that is not following the documentation
and requiring an addon via a different name than that passed as the
first argument to
NODE_MODULE(...)
.Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passes