Skip to content

Commit 3ca00e7

Browse files
src: add support to pass flags to dlopen
This commit introduces an optional parameter for process.dlopen(), allowing to pass dlopen flags (using values from os.constants.dlopen). If no flags are passed, the default behavior is to load the library with RTLD_LAZY (perform lazy binding) and RTLD_LOCAL (symbols are available only locally). Signed-off-by: Ezequiel Garcia <[email protected]>
1 parent 6ec4386 commit 3ca00e7

File tree

6 files changed

+217
-14
lines changed

6 files changed

+217
-14
lines changed

doc/api/process.md

+45
Original file line numberDiff line numberDiff line change
@@ -638,6 +638,48 @@ process's [`ChildProcess.disconnect()`][].
638638
If the Node.js process was not spawned with an IPC channel,
639639
`process.disconnect()` will be `undefined`.
640640

641+
## process.dlopen(module, filename[, flags])
642+
<!-- YAML
643+
added: v0.1.16
644+
changes:
645+
- version: REPLACEME
646+
pr-url: https://github.com/nodejs/node/pull/12794
647+
description: Added support for the `flags` argument.
648+
-->
649+
650+
* `module` {Object}
651+
* `filename` {string}
652+
* `flags` {os.constants.dlopen}. Defaults to `os.constants.dlopen.RTLD_LAZY`.
653+
654+
The `process.dlopen()` method allows to dynamically load shared
655+
objects. It is primarily used by `require()` to load
656+
C++ Addons, and should not be used directly, except in special
657+
cases. In other words, [`require()`][] should be preferred over
658+
`process.dlopen()`, unless there are specific reasons.
659+
660+
The `flags` argument is an integer that allows to specify dlopen
661+
behavior. See the [`os.constants.dlopen`][] documentation for details.
662+
663+
If there are specific reasons to use `process.dlopen()` (for instance,
664+
to specify dlopen flags), it's often useful to use [`require.resolve()`][]
665+
to look up the module's path.
666+
667+
*Note*: An important drawback when calling `process.dlopen()` is that the
668+
`module` instance must be passed. Functions exported by the C++ Addon will
669+
be accessible via `module.exports`.
670+
671+
The example below shows how to load a C++ Addon, named as `binding`,
672+
that exports a `foo` function. All the symbols will be loaded before
673+
the call returns, by passing the `RTLD_NOW` constant. In this example
674+
the constant is assumed to be available.
675+
676+
```js
677+
const os = require('os');
678+
process.dlopen(module, require.resolve('binding'),
679+
os.constants.dlopen.RTLD_NOW);
680+
module.exports.foo();
681+
```
682+
641683
## process.emitWarning(warning[, options])
642684
<!-- YAML
643685
added: 8.0.0
@@ -1841,13 +1883,16 @@ cases:
18411883
[`end()`]: stream.html#stream_writable_end_chunk_encoding_callback
18421884
[`net.Server`]: net.html#net_class_net_server
18431885
[`net.Socket`]: net.html#net_class_net_socket
1886+
[`os.constants.dlopen`]: os.html#os_dlopen_constants
18441887
[`process.argv`]: #process_process_argv
18451888
[`process.execPath`]: #process_process_execpath
18461889
[`process.exit()`]: #process_process_exit_code
18471890
[`process.exitCode`]: #process_process_exitcode
18481891
[`process.kill()`]: #process_process_kill_pid_signal
18491892
[`promise.catch()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
1893+
[`require()`]: globals.html#globals_require
18501894
[`require.main`]: modules.html#modules_accessing_the_main_module
1895+
[`require.resolve()`]: globals.html#globals_require_resolve
18511896
[`setTimeout(fn, 0)`]: timers.html#timers_settimeout_callback_delay_args
18521897
[Child Process]: child_process.html
18531898
[Cluster]: cluster.html

src/node.cc

+67-14
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,10 @@ typedef int mode_t;
117117
#include <grp.h> // getgrnam()
118118
#endif
119119

120+
#if defined(__POSIX__)
121+
#include <dlfcn.h>
122+
#endif
123+
120124
#ifdef __APPLE__
121125
#include <crt_externs.h>
122126
#define environ (*_NSGetEnviron())
@@ -2503,36 +2507,85 @@ struct node_module* get_linked_module(const char* name) {
25032507
return mp;
25042508
}
25052509

2506-
// DLOpen is process.dlopen(module, filename).
2510+
struct DLib {
2511+
std::string filename_;
2512+
std::string errmsg_;
2513+
void* handle_;
2514+
int flags_;
2515+
2516+
#ifdef __POSIX__
2517+
static const int kDefaultFlags = RTLD_LAZY;
2518+
2519+
bool Open() {
2520+
handle_ = dlopen(filename_.c_str(), flags_);
2521+
if (handle_ != nullptr)
2522+
return true;
2523+
errmsg_ = dlerror();
2524+
return false;
2525+
}
2526+
2527+
void Close() {
2528+
if (handle_ != nullptr)
2529+
dlclose(handle_);
2530+
}
2531+
#else // !__POSIX__
2532+
static const int kDefaultFlags = 0;
2533+
uv_lib_t lib_;
2534+
2535+
bool Open() {
2536+
int ret = uv_dlopen(filename_.c_str(), &lib_);
2537+
if (ret == 0) {
2538+
handle_ = static_cast<void*>(lib_.handle);
2539+
return true;
2540+
}
2541+
errmsg_ = uv_dlerror(&lib_);
2542+
uv_dlclose(&lib_);
2543+
return false;
2544+
}
2545+
2546+
void Close() {
2547+
uv_dlclose(&lib_);
2548+
}
2549+
#endif // !__POSIX__
2550+
};
2551+
2552+
// DLOpen is process.dlopen(module, filename, flags).
25072553
// Used to load 'module.node' dynamically shared objects.
25082554
//
25092555
// FIXME(bnoordhuis) Not multi-context ready. TBD how to resolve the conflict
25102556
// when two contexts try to load the same shared object. Maybe have a shadow
25112557
// cache that's a plain C list or hash table that's shared across contexts?
25122558
static void DLOpen(const FunctionCallbackInfo<Value>& args) {
25132559
Environment* env = Environment::GetCurrent(args);
2514-
uv_lib_t lib;
25152560

25162561
CHECK_EQ(modpending, nullptr);
25172562

2518-
if (args.Length() != 2) {
2519-
env->ThrowError("process.dlopen takes exactly 2 arguments.");
2563+
if (args.Length() < 2) {
2564+
env->ThrowError("process.dlopen needs at least 2 arguments.");
25202565
return;
25212566
}
25222567

2568+
int32_t flags = DLib::kDefaultFlags;
2569+
if (args.Length() > 2 && !args[2]->Int32Value(env->context()).To(&flags)) {
2570+
return env->ThrowTypeError("flag argument must be an integer.");
2571+
}
2572+
25232573
Local<Object> module = args[0]->ToObject(env->isolate()); // Cast
25242574
node::Utf8Value filename(env->isolate(), args[1]); // Cast
2525-
const bool is_dlopen_error = uv_dlopen(*filename, &lib);
2575+
DLib dlib;
2576+
dlib.filename_ = *filename;
2577+
dlib.flags_ = flags;
2578+
bool is_opened = dlib.Open();
25262579

25272580
// Objects containing v14 or later modules will have registered themselves
25282581
// on the pending list. Activate all of them now. At present, only one
25292582
// module per object is supported.
25302583
node_module* const mp = modpending;
25312584
modpending = nullptr;
25322585

2533-
if (is_dlopen_error) {
2534-
Local<String> errmsg = OneByteString(env->isolate(), uv_dlerror(&lib));
2535-
uv_dlclose(&lib);
2586+
if (!is_opened) {
2587+
Local<String> errmsg = OneByteString(env->isolate(), dlib.errmsg_.c_str());
2588+
dlib.Close();
25362589
#ifdef _WIN32
25372590
// Windows needs to add the filename into the error message
25382591
errmsg = String::Concat(errmsg, args[1]->ToString(env->isolate()));
@@ -2542,7 +2595,7 @@ static void DLOpen(const FunctionCallbackInfo<Value>& args) {
25422595
}
25432596

25442597
if (mp == nullptr) {
2545-
uv_dlclose(&lib);
2598+
dlib.Close();
25462599
env->ThrowError("Module did not self-register.");
25472600
return;
25482601
}
@@ -2569,18 +2622,18 @@ static void DLOpen(const FunctionCallbackInfo<Value>& args) {
25692622
}
25702623

25712624
// NOTE: `mp` is allocated inside of the shared library's memory, calling
2572-
// `uv_dlclose` will deallocate it
2573-
uv_dlclose(&lib);
2625+
// `dlclose` will deallocate it
2626+
dlib.Close();
25742627
env->ThrowError(errmsg);
25752628
return;
25762629
}
25772630
if (mp->nm_flags & NM_F_BUILTIN) {
2578-
uv_dlclose(&lib);
2631+
dlib.Close();
25792632
env->ThrowError("Built-in module self-registered.");
25802633
return;
25812634
}
25822635

2583-
mp->nm_dso_handle = lib.handle;
2636+
mp->nm_dso_handle = dlib.handle_;
25842637
mp->nm_link = modlist_addon;
25852638
modlist_addon = mp;
25862639

@@ -2592,7 +2645,7 @@ static void DLOpen(const FunctionCallbackInfo<Value>& args) {
25922645
} else if (mp->nm_register_func != nullptr) {
25932646
mp->nm_register_func(exports, module, mp->nm_priv);
25942647
} else {
2595-
uv_dlclose(&lib);
2648+
dlib.Close();
25962649
env->ThrowError("Module has no declared entry point.");
25972650
return;
25982651
}
+48
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
#include <node.h>
2+
#include <v8.h>
3+
4+
#ifndef _WIN32
5+
6+
#include <dlfcn.h>
7+
8+
extern "C" const char* dlopen_pong(void) {
9+
return "pong";
10+
}
11+
12+
namespace {
13+
14+
using v8::FunctionCallbackInfo;
15+
using v8::Isolate;
16+
using v8::Local;
17+
using v8::Object;
18+
using v8::String;
19+
using v8::Value;
20+
21+
typedef const char* (*ping)(void);
22+
23+
static ping ping_func;
24+
25+
void LoadLibrary(const FunctionCallbackInfo<Value>& args) {
26+
const String::Utf8Value filename(args[0]);
27+
void* handle = dlopen(*filename, RTLD_LAZY);
28+
assert(handle != nullptr);
29+
ping_func = reinterpret_cast<ping>(dlsym(handle, "dlopen_ping"));
30+
assert(ping_func != nullptr);
31+
}
32+
33+
void Ping(const FunctionCallbackInfo<Value>& args) {
34+
Isolate* isolate = args.GetIsolate();
35+
assert(ping_func != nullptr);
36+
args.GetReturnValue().Set(String::NewFromUtf8(isolate, ping_func()));
37+
}
38+
39+
void init(Local<Object> exports) {
40+
NODE_SET_METHOD(exports, "load", LoadLibrary);
41+
NODE_SET_METHOD(exports, "ping", Ping);
42+
}
43+
44+
NODE_MODULE(binding, init)
45+
46+
} // anonymous namespace
47+
48+
#endif // _WIN32
+25
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
{
2+
'targets': [
3+
{
4+
'target_name': 'ping',
5+
'product_extension': 'so',
6+
'type': 'shared_library',
7+
'sources': [ 'ping.c' ],
8+
'conditions': [
9+
['OS=="mac"', {
10+
'xcode_settings': {
11+
'OTHER_LDFLAGS': [ '-Wl,-undefined', '-Wl,dynamic_lookup' ]
12+
}}],
13+
# Pass erok flag to the linker, to prevent unresolved symbols
14+
# from failing. Still, the test won't pass, so we'll skip it on AIX.
15+
['OS=="aix"', {
16+
'ldflags': [ '-Wl,-berok' ]
17+
}]],
18+
},
19+
{
20+
'target_name': 'binding',
21+
'defines': [ 'V8_DEPRECATION_WARNINGS=1' ],
22+
'sources': [ 'binding.cc' ],
23+
}
24+
]
25+
}

test/addons/dlopen-ping-pong/ping.c

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
#ifndef _WIN32
2+
3+
const char* dlopen_pong(void);
4+
5+
const char* dlopen_ping(void) {
6+
return dlopen_pong();
7+
}
8+
9+
#endif // _WIN32

test/addons/dlopen-ping-pong/test.js

+23
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
'use strict';
2+
const common = require('../../common');
3+
4+
if (common.isWindows)
5+
common.skip('dlopen global symbol loading is not supported on this os.');
6+
7+
if (common.isAIX)
8+
common.skip('this test does not pass on AIX.');
9+
10+
const assert = require('assert');
11+
const path = require('path');
12+
const os = require('os');
13+
14+
const bindingPath = require.resolve(`./build/${common.buildType}/binding`);
15+
process.dlopen(module, bindingPath,
16+
os.constants.dlopen.RTLD_NOW | os.constants.dlopen.RTLD_GLOBAL);
17+
module.exports.load(path.dirname(bindingPath) + '/ping.so');
18+
assert.strictEqual(module.exports.ping(), 'pong');
19+
20+
// Check that after the addon is loaded with
21+
// process.dlopen() a require() call fails.
22+
const re = /^Error: Module did not self-register\.$/;
23+
assert.throws(() => require(`./build/${common.buildType}/binding`), re);

0 commit comments

Comments
 (0)