Skip to content

Commit

Permalink
src: add support to pass flags to dlopen
Browse files Browse the repository at this point in the history
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]>
  • Loading branch information
ezequielgarcia committed Sep 8, 2017
1 parent 6ec4386 commit 3ca00e7
Show file tree
Hide file tree
Showing 6 changed files with 217 additions and 14 deletions.
45 changes: 45 additions & 0 deletions doc/api/process.md
Original file line number Diff line number Diff line change
Expand Up @@ -638,6 +638,48 @@ process's [`ChildProcess.disconnect()`][].
If the Node.js process was not spawned with an IPC channel,
`process.disconnect()` will be `undefined`.

## process.dlopen(module, filename[, flags])
<!-- YAML
added: v0.1.16
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/12794
description: Added support for the `flags` argument.
-->

* `module` {Object}
* `filename` {string}
* `flags` {os.constants.dlopen}. Defaults to `os.constants.dlopen.RTLD_LAZY`.

The `process.dlopen()` method allows to dynamically load shared
objects. It is primarily used by `require()` to load
C++ Addons, and should not be used directly, except in special
cases. In other words, [`require()`][] should be preferred over
`process.dlopen()`, unless there are specific reasons.

The `flags` argument is an integer that allows to specify dlopen
behavior. See the [`os.constants.dlopen`][] documentation for details.

If there are specific reasons to use `process.dlopen()` (for instance,
to specify dlopen flags), it's often useful to use [`require.resolve()`][]
to look up the module's path.

*Note*: An important drawback when calling `process.dlopen()` is that the
`module` instance must be passed. Functions exported by the C++ Addon will
be accessible via `module.exports`.

The example below shows how to load a C++ Addon, named as `binding`,
that exports a `foo` function. All the symbols will be loaded before
the call returns, by passing the `RTLD_NOW` constant. In this example
the constant is assumed to be available.

```js
const os = require('os');
process.dlopen(module, require.resolve('binding'),
os.constants.dlopen.RTLD_NOW);
module.exports.foo();
```

## process.emitWarning(warning[, options])
<!-- YAML
added: 8.0.0
Expand Down Expand Up @@ -1841,13 +1883,16 @@ cases:
[`end()`]: stream.html#stream_writable_end_chunk_encoding_callback
[`net.Server`]: net.html#net_class_net_server
[`net.Socket`]: net.html#net_class_net_socket
[`os.constants.dlopen`]: os.html#os_dlopen_constants
[`process.argv`]: #process_process_argv
[`process.execPath`]: #process_process_execpath
[`process.exit()`]: #process_process_exit_code
[`process.exitCode`]: #process_process_exitcode
[`process.kill()`]: #process_process_kill_pid_signal
[`promise.catch()`]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/catch
[`require()`]: globals.html#globals_require
[`require.main`]: modules.html#modules_accessing_the_main_module
[`require.resolve()`]: globals.html#globals_require_resolve
[`setTimeout(fn, 0)`]: timers.html#timers_settimeout_callback_delay_args
[Child Process]: child_process.html
[Cluster]: cluster.html
Expand Down
81 changes: 67 additions & 14 deletions src/node.cc
Original file line number Diff line number Diff line change
Expand Up @@ -117,6 +117,10 @@ typedef int mode_t;
#include <grp.h> // getgrnam()
#endif

#if defined(__POSIX__)
#include <dlfcn.h>
#endif

#ifdef __APPLE__
#include <crt_externs.h>
#define environ (*_NSGetEnviron())
Expand Down Expand Up @@ -2503,36 +2507,85 @@ struct node_module* get_linked_module(const char* name) {
return mp;
}

// DLOpen is process.dlopen(module, filename).
struct DLib {
std::string filename_;
std::string errmsg_;
void* handle_;
int flags_;

#ifdef __POSIX__
static const int kDefaultFlags = RTLD_LAZY;

bool Open() {
handle_ = dlopen(filename_.c_str(), flags_);
if (handle_ != nullptr)
return true;
errmsg_ = dlerror();
return false;
}

void Close() {
if (handle_ != nullptr)
dlclose(handle_);
}
#else // !__POSIX__
static const int kDefaultFlags = 0;
uv_lib_t lib_;

bool Open() {
int ret = uv_dlopen(filename_.c_str(), &lib_);
if (ret == 0) {
handle_ = static_cast<void*>(lib_.handle);
return true;
}
errmsg_ = uv_dlerror(&lib_);
uv_dlclose(&lib_);
return false;
}

void Close() {
uv_dlclose(&lib_);
}
#endif // !__POSIX__
};

// DLOpen is process.dlopen(module, filename, flags).
// Used to load 'module.node' dynamically shared objects.
//
// FIXME(bnoordhuis) Not multi-context ready. TBD how to resolve the conflict
// when two contexts try to load the same shared object. Maybe have a shadow
// cache that's a plain C list or hash table that's shared across contexts?
static void DLOpen(const FunctionCallbackInfo<Value>& args) {
Environment* env = Environment::GetCurrent(args);
uv_lib_t lib;

CHECK_EQ(modpending, nullptr);

if (args.Length() != 2) {
env->ThrowError("process.dlopen takes exactly 2 arguments.");
if (args.Length() < 2) {
env->ThrowError("process.dlopen needs at least 2 arguments.");
return;
}

int32_t flags = DLib::kDefaultFlags;
if (args.Length() > 2 && !args[2]->Int32Value(env->context()).To(&flags)) {
return env->ThrowTypeError("flag argument must be an integer.");
}

Local<Object> module = args[0]->ToObject(env->isolate()); // Cast
node::Utf8Value filename(env->isolate(), args[1]); // Cast
const bool is_dlopen_error = uv_dlopen(*filename, &lib);
DLib dlib;
dlib.filename_ = *filename;
dlib.flags_ = flags;
bool is_opened = dlib.Open();

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

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

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

// NOTE: `mp` is allocated inside of the shared library's memory, calling
// `uv_dlclose` will deallocate it
uv_dlclose(&lib);
// `dlclose` will deallocate it
dlib.Close();
env->ThrowError(errmsg);
return;
}
if (mp->nm_flags & NM_F_BUILTIN) {
uv_dlclose(&lib);
dlib.Close();
env->ThrowError("Built-in module self-registered.");
return;
}

mp->nm_dso_handle = lib.handle;
mp->nm_dso_handle = dlib.handle_;
mp->nm_link = modlist_addon;
modlist_addon = mp;

Expand All @@ -2592,7 +2645,7 @@ static void DLOpen(const FunctionCallbackInfo<Value>& args) {
} else if (mp->nm_register_func != nullptr) {
mp->nm_register_func(exports, module, mp->nm_priv);
} else {
uv_dlclose(&lib);
dlib.Close();
env->ThrowError("Module has no declared entry point.");
return;
}
Expand Down
48 changes: 48 additions & 0 deletions test/addons/dlopen-ping-pong/binding.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
#include <node.h>
#include <v8.h>

#ifndef _WIN32

#include <dlfcn.h>

extern "C" const char* dlopen_pong(void) {
return "pong";
}

namespace {

using v8::FunctionCallbackInfo;
using v8::Isolate;
using v8::Local;
using v8::Object;
using v8::String;
using v8::Value;

typedef const char* (*ping)(void);

static ping ping_func;

void LoadLibrary(const FunctionCallbackInfo<Value>& args) {
const String::Utf8Value filename(args[0]);
void* handle = dlopen(*filename, RTLD_LAZY);
assert(handle != nullptr);
ping_func = reinterpret_cast<ping>(dlsym(handle, "dlopen_ping"));
assert(ping_func != nullptr);
}

void Ping(const FunctionCallbackInfo<Value>& args) {
Isolate* isolate = args.GetIsolate();
assert(ping_func != nullptr);
args.GetReturnValue().Set(String::NewFromUtf8(isolate, ping_func()));
}

void init(Local<Object> exports) {
NODE_SET_METHOD(exports, "load", LoadLibrary);
NODE_SET_METHOD(exports, "ping", Ping);
}

NODE_MODULE(binding, init)

} // anonymous namespace

#endif // _WIN32
25 changes: 25 additions & 0 deletions test/addons/dlopen-ping-pong/binding.gyp
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
{
'targets': [
{
'target_name': 'ping',
'product_extension': 'so',
'type': 'shared_library',
'sources': [ 'ping.c' ],
'conditions': [
['OS=="mac"', {
'xcode_settings': {
'OTHER_LDFLAGS': [ '-Wl,-undefined', '-Wl,dynamic_lookup' ]
}}],
# Pass erok flag to the linker, to prevent unresolved symbols
# from failing. Still, the test won't pass, so we'll skip it on AIX.
['OS=="aix"', {
'ldflags': [ '-Wl,-berok' ]
}]],
},
{
'target_name': 'binding',
'defines': [ 'V8_DEPRECATION_WARNINGS=1' ],
'sources': [ 'binding.cc' ],
}
]
}
9 changes: 9 additions & 0 deletions test/addons/dlopen-ping-pong/ping.c
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
#ifndef _WIN32

const char* dlopen_pong(void);

const char* dlopen_ping(void) {
return dlopen_pong();
}

#endif // _WIN32
23 changes: 23 additions & 0 deletions test/addons/dlopen-ping-pong/test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
'use strict';
const common = require('../../common');

if (common.isWindows)
common.skip('dlopen global symbol loading is not supported on this os.');

if (common.isAIX)
common.skip('this test does not pass on AIX.');

const assert = require('assert');
const path = require('path');
const os = require('os');

const bindingPath = require.resolve(`./build/${common.buildType}/binding`);
process.dlopen(module, bindingPath,
os.constants.dlopen.RTLD_NOW | os.constants.dlopen.RTLD_GLOBAL);
module.exports.load(path.dirname(bindingPath) + '/ping.so');
assert.strictEqual(module.exports.ping(), 'pong');

// Check that after the addon is loaded with
// process.dlopen() a require() call fails.
const re = /^Error: Module did not self-register\.$/;
assert.throws(() => require(`./build/${common.buildType}/binding`), re);

0 comments on commit 3ca00e7

Please sign in to comment.