Skip to content

Commit 22d2284

Browse files
committed
worker: improve integration with native addons
Allow loading add-ons from multiple Node.js instances if they are declared context-aware; in particular, this applies to N-API addons. Also, plug a memory leak that occurred when registering N-API addons. Refs: nodejs#23319
1 parent c077c21 commit 22d2284

File tree

10 files changed

+116
-22
lines changed

10 files changed

+116
-22
lines changed

doc/api/addons.md

+5
Original file line numberDiff line numberDiff line change
@@ -251,6 +251,11 @@ down. If necessary, such hooks can be removed using
251251
`RemoveEnvironmentCleanupHook()` before they are run, which has the same
252252
signature.
253253
254+
In order to be loaded from multiple Node.js environments,
255+
such as a main thread and a Worker thread, an add-on needs to either:
256+
- Be an N-API addon, or
257+
- Be declared as context-aware using `NODE_MODULE_INIT()` as described above
258+
254259
### Building
255260
256261
Once the source code has been written, it must be compiled into the binary

doc/api/worker_threads.md

+3-1
Original file line numberDiff line numberDiff line change
@@ -360,11 +360,12 @@ Notable differences inside a Worker environment are:
360360
being invoked.
361361
- IPC channels from parent processes are not accessible.
362362
- The [`trace_events`][] module is not supported.
363+
- Native add-ons can only be loaded from multiple threads if they fulfill
364+
[certain conditions][Addons worker support].
363365

364366
Currently, the following differences also exist until they are addressed:
365367

366368
- The [`inspector`][] module is not available yet.
367-
- Native addons are not supported yet.
368369

369370
Creating `Worker` instances inside of other `Worker`s is possible.
370371

@@ -602,6 +603,7 @@ active handle in the event system. If the worker is already `unref()`ed calling
602603
[`worker.postMessage()`]: #worker_threads_worker_postmessage_value_transferlist
603604
[`worker.terminate()`]: #worker_threads_worker_terminate_callback
604605
[`worker.threadId`]: #worker_threads_worker_threadid_1
606+
[Addons worker support]: addons.html#addons_worker_support
605607
[HTML structured clone algorithm]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
606608
[Signals events]: process.html#process_signal_events
607609
[Web Workers]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API

src/node_api.cc

+1-1
Original file line numberDiff line numberDiff line change
@@ -484,7 +484,7 @@ void napi_module_register_by_symbol(v8::Local<v8::Object> exports,
484484
void napi_module_register(napi_module* mod) {
485485
node::node_module* nm = new node::node_module {
486486
-1,
487-
mod->nm_flags,
487+
mod->nm_flags | NM_F_DELETEME,
488488
nullptr,
489489
mod->nm_filename,
490490
nullptr,

src/node_binding.cc

+56-11
Original file line numberDiff line numberDiff line change
@@ -103,7 +103,6 @@ using v8::Value;
103103
// Globals per process
104104
static node_module* modlist_internal;
105105
static node_module* modlist_linked;
106-
static node_module* modlist_addon;
107106
static uv_once_t init_modpending_once = UV_ONCE_INIT;
108107
static uv_key_t thread_local_modpending;
109108

@@ -129,6 +128,48 @@ extern "C" void node_module_register(void* m) {
129128

130129
namespace binding {
131130

131+
static struct global_handle_map_t {
132+
public:
133+
void set(void* handle, node_module* mod) {
134+
CHECK_NE(handle, nullptr);
135+
Mutex::ScopedLock lock(mutex_);
136+
137+
map_[handle] = Entry { 1, mod };
138+
}
139+
140+
node_module* get_and_increase_refcount(void* handle) {
141+
CHECK_NE(handle, nullptr);
142+
Mutex::ScopedLock lock(mutex_);
143+
144+
auto it = map_.find(handle);
145+
if (it == map_.end()) return nullptr;
146+
it->second.refcount++;
147+
return it->second.module;
148+
}
149+
150+
void erase(void* handle) {
151+
CHECK_NE(handle, nullptr);
152+
Mutex::ScopedLock lock(mutex_);
153+
154+
auto it = map_.find(handle);
155+
if (it == map_.end()) return;
156+
CHECK_GE(it->second.refcount, 1);
157+
if (--it->second.refcount == 0) {
158+
if (it->second.module->nm_flags & NM_F_DELETEME)
159+
delete it->second.module;
160+
map_.erase(handle);
161+
}
162+
}
163+
164+
private:
165+
Mutex mutex_;
166+
struct Entry {
167+
unsigned int refcount;
168+
node_module* module;
169+
};
170+
std::unordered_map<void*, Entry> map_;
171+
} global_handle_map;
172+
132173
DLib::DLib(const char* filename, int flags)
133174
: filename_(filename), flags_(flags), handle_(nullptr) {}
134175

@@ -142,6 +183,7 @@ bool DLib::Open() {
142183

143184
void DLib::Close() {
144185
if (handle_ == nullptr) return;
186+
global_handle_map.erase(handle_);
145187
dlclose(handle_);
146188
handle_ = nullptr;
147189
}
@@ -233,7 +275,7 @@ void DLOpen(const FunctionCallbackInfo<Value>& args) {
233275
// Objects containing v14 or later modules will have registered themselves
234276
// on the pending list. Activate all of them now. At present, only one
235277
// module per object is supported.
236-
node_module* const mp =
278+
node_module* mp =
237279
static_cast<node_module*>(uv_key_get(&thread_local_modpending));
238280
uv_key_set(&thread_local_modpending, nullptr);
239281

@@ -250,17 +292,24 @@ void DLOpen(const FunctionCallbackInfo<Value>& args) {
250292
return false;
251293
}
252294

253-
if (mp == nullptr) {
295+
if (mp != nullptr) {
296+
mp->nm_dso_handle = dlib->handle_;
297+
global_handle_map.set(dlib->handle_, mp);
298+
} else {
254299
if (auto callback = GetInitializerCallback(dlib)) {
255300
callback(exports, module, context);
301+
return true;
256302
} else if (auto napi_callback = GetNapiInitializerCallback(dlib)) {
257303
napi_module_register_by_symbol(exports, module, context, napi_callback);
304+
return true;
258305
} else {
259-
dlib->Close();
260-
env->ThrowError("Module did not self-register.");
261-
return false;
306+
mp = global_handle_map.get_and_increase_refcount(dlib->handle_);
307+
if (mp == nullptr || mp->nm_context_register_func == nullptr) {
308+
dlib->Close();
309+
env->ThrowError("Module did not self-register.");
310+
return false;
311+
}
262312
}
263-
return true;
264313
}
265314

266315
// -1 is used for N-API modules
@@ -293,10 +342,6 @@ void DLOpen(const FunctionCallbackInfo<Value>& args) {
293342
}
294343
CHECK_EQ(mp->nm_flags & NM_F_BUILTIN, 0);
295344

296-
mp->nm_dso_handle = dlib->handle_;
297-
mp->nm_link = modlist_addon;
298-
modlist_addon = mp;
299-
300345
if (mp->nm_context_register_func != nullptr) {
301346
mp->nm_context_register_func(exports, module, context, mp->nm_priv);
302347
} else if (mp->nm_register_func != nullptr) {

src/node_binding.h

+1
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ enum {
1818
NM_F_BUILTIN = 1 << 0, // Unused.
1919
NM_F_LINKED = 1 << 1,
2020
NM_F_INTERNAL = 1 << 2,
21+
NM_F_DELETEME = 1 << 3,
2122
};
2223

2324
#define NODE_MODULE_CONTEXT_AWARE_CPP(modname, regfunc, priv, flags) \
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
'use strict';
2+
const common = require('../../common');
3+
const assert = require('assert');
4+
const { Worker } = require('worker_threads');
5+
6+
// Check that modules that are not declared as context-aware cannot be re-loaded
7+
// from workers.
8+
9+
const bindingPath = require.resolve(`./build/${common.buildType}/binding`);
10+
require(bindingPath);
11+
12+
new Worker(`require(${JSON.stringify(bindingPath)})`, { eval: true })
13+
.on('error', common.mustCall((err) => {
14+
assert.strictEqual(err.constructor, Error);
15+
assert.strictEqual(err.message, 'Module did not self-register.');
16+
}));

test/addons/worker-addon/test.js

+18-9
Original file line numberDiff line numberDiff line change
@@ -6,21 +6,30 @@ const path = require('path');
66
const { Worker } = require('worker_threads');
77
const binding = path.resolve(__dirname, `./build/${common.buildType}/binding`);
88

9-
if (process.argv[2] === 'worker') {
10-
new Worker(`require(${JSON.stringify(binding)});`, { eval: true });
11-
return;
12-
} else if (process.argv[2] === 'main-thread') {
13-
process.env.addExtraItemToEventLoop = 'yes';
14-
require(binding);
15-
return;
9+
switch (process.argv[2]) {
10+
case 'both':
11+
require(binding);
12+
// fallthrough
13+
case 'worker':
14+
new Worker(`require(${JSON.stringify(binding)});`, { eval: true });
15+
return;
16+
case 'main-thread':
17+
process.env.addExtraItemToEventLoop = 'yes';
18+
require(binding);
19+
return;
1620
}
1721

18-
for (const test of ['worker', 'main-thread']) {
22+
for (const test of ['worker', 'main-thread', 'both']) {
1923
const proc = child_process.spawnSync(process.execPath, [
2024
__filename,
2125
test
2226
]);
2327
assert.strictEqual(proc.stderr.toString(), '');
24-
assert.strictEqual(proc.stdout.toString(), 'ctor cleanup dtor');
28+
// We always only have 1 instance of the shared object in memory, so
29+
// 1 ctor and 1 dtor call. If we attach the module to 2 Environments,
30+
// we expect 2 cleanup calls, otherwise one.
31+
assert.strictEqual(
32+
proc.stdout.toString(),
33+
test === 'both' ? 'ctor cleanup cleanup dtor' : 'ctor cleanup dtor');
2534
assert.strictEqual(proc.status, 0);
2635
}

test/node-api/1_hello_world/test.js

+9
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
'use strict';
22
const common = require('../../common');
33
const assert = require('assert');
4+
const { Worker } = require('worker_threads');
5+
46
const bindingPath = require.resolve(`./build/${common.buildType}/binding`);
57
const binding = require(bindingPath);
68
assert.strictEqual(binding.hello(), 'world');
@@ -11,3 +13,10 @@ delete require.cache[bindingPath];
1113
const rebinding = require(bindingPath);
1214
assert.strictEqual(rebinding.hello(), 'world');
1315
assert.notStrictEqual(binding.hello, rebinding.hello);
16+
17+
// Test that workers can load addons declared using NAPI_MODULE_INIT().
18+
new Worker(`
19+
const { parentPort } = require('worker_threads');
20+
const msg = require(${JSON.stringify(bindingPath)}).hello();
21+
parentPort.postMessage(msg)`, { eval: true })
22+
.on('message', common.mustCall((msg) => assert.strictEqual(msg, 'world')));

test/node-api/test_worker_terminate/test.js

+5
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,11 @@ const assert = require('assert');
44
const { Worker, isMainThread, workerData } = require('worker_threads');
55

66
if (isMainThread) {
7+
// Load the addon in the main thread first.
8+
// This checks that N-API addons can be loaded from multiple contexts
9+
// when they are not loaded through NAPI_MODULE().
10+
require(`./build/${common.buildType}/test_worker_terminate`);
11+
712
const counter = new Int32Array(new SharedArrayBuffer(4));
813
const worker = new Worker(__filename, { workerData: { counter } });
914
worker.on('exit', common.mustCall(() => {

test/node-api/test_worker_terminate/test_worker_terminate.c

+2
Original file line numberDiff line numberDiff line change
@@ -36,4 +36,6 @@ napi_value Init(napi_env env, napi_value exports) {
3636
return exports;
3737
}
3838

39+
// Do not start using NAPI_MODULE_INIT() here, so that we can test
40+
// compatibility of Workers with NAPI_MODULE().
3941
NAPI_MODULE(NODE_GYP_MODULE_NAME, Init)

0 commit comments

Comments
 (0)