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

src: add addon entry point symbol cache #20759

Closed
wants to merge 1 commit into from

Conversation

gabrielschulhof
Copy link
Contributor

Determine whether the dlopen() of a certain addon is the first load of
said addon, or if the addon had previously been loaded. This is
accomplished by storing the addon's entry point in a set. If, upon a
subsequent load, after determining the addon's entry point, it is found
to already be present in the cache, then the handle returned by the
subsequent dlopen() is closed.

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

@nodejs-github-bot nodejs-github-bot added c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory. labels May 15, 2018
Copy link
Member

@addaleax addaleax left a comment

Choose a reason for hiding this comment

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

It’s hard to tell the actual behavioural differences here – as far as I can tell:

  • We call .Close() if we have already loaded the addon. I think at least on POSIX systems that won’t make much of a difference?
  • The bailout when an N-API addon has a NULL entry point. That seems like a good idea, but I’m not sure how it depends on the rest?

src/node.cc Outdated
} else { \
addon_entry_points.insert((key_value)); \
} \
} while (0)
Copy link
Member

Choose a reason for hiding this comment

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

Can this rather be an inline function?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not sure how to capture all the different ways we initialize an addon in a single inline function.

We have

  • callback(exports, module, context) for well-known native addon symbols
  • napi_module_register_by_symbol(exports, module, context, napi_callback) for N-API
  • mp->nm_context_register_func(exports, module, context, mp->nm_priv) for self-registering context-aware addons
  • mp->nm_register_func(exports, module, mp->nm_priv) for self-registering non-context-aware addons

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Making some headway with lambdas. Lessee ...

src/node.cc Outdated
CALL_ADDON_INIT(
napi_module_register_by_symbol(exports, module, context,
napi_callback),
napi_callback, dlib);
Copy link
Member

Choose a reason for hiding this comment

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

Style nit: Indentation is off here

@addaleax
Copy link
Member

Oh, also: Maybe take a look at addaleax/node@1d8b74d. It’s WIP and not going to be PR’ed soon, but that’s the direction into which I’d like to move the addon loading mechanism :)

src/node.cc Outdated
if (addon_entry_points.count((key_value)) == 1) { \
(dlib).Close(); \
} else { \
addon_entry_points.insert((key_value)); \
Copy link
Member

Choose a reason for hiding this comment

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

Can be DRY'd to this:

if (!addon_entry_points.insert(key_value).second) dlib.Close();

Apropos the name key_value, that to me suggests it's a std::pair when it's not.

@gabrielschulhof
Copy link
Contributor Author

gabrielschulhof commented May 16, 2018

@addaleax I've left a comment on your commit: addaleax@1d8b74d#commitcomment-28999355. tl;dr: Awesome work, but please use the module entry point rather than the module file name as the key.

@gabrielschulhof
Copy link
Contributor Author

@addaleax I've replaced the macro with an inline function.

@bnoordhuis I've switched to entry_point.

@gabrielschulhof
Copy link
Contributor Author

@addaleax I've separated out the N-API null-checking into its own PR.

@fs-eire
Copy link
Contributor

fs-eire commented May 18, 2018

does this change also allows an addon defined by NODE_MODULE() being loaded in multiple context?

@gabrielschulhof
Copy link
Contributor Author

gabrielschulhof commented May 18, 2018

@fs-eire the ability to re-initialize an addon, possibly with a different context, has already been available since 3828fc6.

This change merely makes sure that dlopen() handles do not leak as you load a given module multiple times.

So, with this change, for any given module, if dlopen() is called more than once, then the second and subsequent times dlclose() will be called immediately afterwards, thereby ensuring that only the first dlopen() handle remains open.

@gabrielschulhof
Copy link
Contributor Author

Running CI https://ci.nodejs.org/job/node-test-pull-request/14966/ to see if there are any platform-specific issues.

@fs-eire
Copy link
Contributor

fs-eire commented May 19, 2018

@gabrielschulhof
my understanding is, as it described, commit 3828fc6 offered a way to load the symbol node_register_module_v${NODE_MODULE_VERSION}. This change should not affect the behavior of an addon defined by macro NODE_MODULE().

@gabrielschulhof
Copy link
Contributor Author

@fs-eire this change does not affect the behaviour of addons. As far as addons are concerned, there is no change. The cleanup is internal, and only done if the addon is loaded a second time.

@BridgeAR
Copy link
Member

Any progress here?

@gabrielschulhof gabrielschulhof force-pushed the symbol-cache branch 2 times, most recently from 50a57ee to 6227204 Compare June 21, 2018 03:03
@gabrielschulhof
Copy link
Contributor Author

@addaleax @bnoordhuis could you please take another look?

1 similar comment
@gabrielschulhof
Copy link
Contributor Author

@addaleax @bnoordhuis could you please take another look?

Copy link
Member

@addaleax addaleax left a comment

Choose a reason for hiding this comment

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

@gabrielschulhof Can you explain how to trigger this condition? Ideally also add a test case? That would help a lot

@@ -2257,6 +2258,22 @@ inline napi_addon_register_func GetNapiInitializerCallback(DLib* dlib) {
reinterpret_cast<napi_addon_register_func>(dlib->GetSymbolAddress(name));
}

// Initialize the addon, and close the DLib if this is a second-or-later
// initialization call.
Copy link
Member

Choose a reason for hiding this comment

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

Shouldn’t it be the reverse situation? Why do we initialize the addon and then close it?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

We do not close the addon. We close the dlopen() handle, because, if the symbol was already in the cache, we know that we've obtainted a dlopen() handle to it in the past, and thus we can conclude that the dlopen() handle we received this time is not a new handle, the closing of which would cause the addon to be unmapped from our address space, but rather a refcounted version of the old handle we received in the past. Thus, closing the new handle is safe.

Determine whether the dlopen() of a certain addon is the first load of
said addon, or if the addon had previously been loaded. This is
accomplished by storing the addon's entry point in a set. If, upon a
subsequent load, after determining the addon's entry point, it is found
to already be present in the cache, then the handle returned by the
subsequent dlopen() is closed.
@gabrielschulhof
Copy link
Contributor Author

@addaleax test/addons/hello-world already uses the well-known symbol mechanism, and it loads the addon twice. So, it triggers this condition.

I'm not sure how to produce a testable result as a function of whether dlib.Close() gets called or not, because I don't have a hook by which to feed something back into JS for an assert().

If you run ./node_g test/addons/hello-world/test.js with gdb, step through DLopen() and print dlib before and after CallAddonInitializer() you will see that, during the first call to DLopen(), dlib.handle_ retains its value after CallAddonInitializer() completes, whereas during the second call to DLopen(), calling CallAddonInitalizer() causes dlib.handle_ to become nullptr - meaning that dlib.Close() was called from within CallAddonInitializer().

This ensures that we do not leak libdl refcounts for the addons we load.

@gabrielschulhof
Copy link
Contributor Author

In retrospect, I believe the correct way of handling multiple dlopen()s of the same module is to allow it, save the dlopen() handle in a private property of the module, and implement module unloading, during the process of which the dlopen() handle will be retrieved from the module's private property in the weak callback, and passed to dlclose().

@gabrielschulhof gabrielschulhof deleted the symbol-cache branch July 24, 2018 02:28
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
c++ Issues and PRs that require attention from people who are familiar with C++. lib / src Issues and PRs related to general changes in the lib or src directory.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

6 participants