Skip to content

Commit

Permalink
src: add context-aware init macro and doc
Browse files Browse the repository at this point in the history
Introduces macro `NODE_MODULE_INIT()` which creates the boilerplate for
a context-aware module which can be loaded multiple times via the
special symbol mechanism.

Additionally, provides an example of using the new macro to construct
an addon which stores per-addon-instance data in a heap-allocated
structure that gets passed to each binding, rather than in a collection
of global static variables.

Re: nodejs/node#21291 (comment)
  • Loading branch information
Gabriel Schulhof committed Jul 1, 2018
1 parent 81f06ba commit e9a452b
Show file tree
Hide file tree
Showing 3 changed files with 140 additions and 7 deletions.
112 changes: 112 additions & 0 deletions doc/api/addons.md
Original file line number Diff line number Diff line change
Expand Up @@ -98,6 +98,118 @@ the `.node` suffix).
In the `hello.cc` example, then, the initialization function is `Initialize`
and the addon module name is `addon`.

When building addons with `node-gyp`, using the macro `NODE_GYP_MODULE_NAME` as
the first parameter of `NODE_MODULE()` will ensure that the name of the final
binary will be passed to `NODE_MODULE()`.

### Context-aware addons

There are environments in which Node.js addons may need to be loaded multiple
times in multiple contexts. The macro `NODE_MODULE_INIT()` will construct an
addon which supports such a use case. Unlike `NODE_MODULE()`, which is used to
construct an addon around an addon initializer such as `Initialize` in the
above example, `NODE_MODULE_INIT()` serves as the declaration of such an
initializer to be followed by a function body.

The following three variables may be used inside the function body following an
invocation of `NODE_MODULE_INIT()`:
* `Local<Object> exports`,
* `Local<Value> module`, and
* `Local<Context> context`

The choice to build a context-aware addon carries with it the responsibility of
carefully managing global static data. Since the addon may be loaded multiple
times, potentially even from different threads, any global static data stored
in the addon must be properly protected, and must not contain any persistent
references to JavaScript objects. The reason for this is that JavaScript
objects are only valid in one context, and will likely cause a crash when
accessed from the wrong context or from a different thread than the one on which
they were created.

The context-aware addon can be structured to avoid global static data by
performing the following steps:
* defining a class which will hold per-addon-instance data. Such
a class should include a `v8::Persistent<v8::Object>` which will hold a weak
reference to the addon's `exports` object. The callback associated with the weak
reference will then destroy the instance of the class.
* constructing an instance of this class in the addon initializer such that the
`v8::Persistent<v8::Object>` is set to the `exports` object.
* storing the instance of the class in a `v8::External`, and
* passing the `v8::External` to all methods exposed to JavaScript by passing it
to the `v8::FunctionTemplate` constructor which creates the native-backed
JavaScript functions. The `v8::FunctionTemplate` constructor's third parameter
accepts the `v8::External`.

This will ensure that the per-addon-instance data reaches each binding that can
be called from JavaScript. The per-addon-instance data must also be passed into
any asynchronous callbacks the addon may create.

The following example illustrates the implementation of a context-aware addon:

```cpp
#include <node.h>

using namespace v8;

class AddonData {
public:
AddonData(Isolate* isolate, Local<Object> exports):
call_count(0) {
// Link the existence of this object instance to the existence of exports.
exports_.Reset(isolate, exports);
exports_.SetWeak(this, DeleteMe, WeakCallbackType::kParameter);
}

~AddonData() {
if (!exports_.IsEmpty()) {
// Reset the reference to avoid leaking data.
exports_.ClearWeak();
exports_.Reset();
}
}

// Per-addon data.
int call_count;

private:
// Method to call when "exports" is about to be garbage-collected.
static void DeleteMe(const WeakCallbackInfo<AddonData>& info) {
delete info.GetParameter();
}

// Weak handle to the "exports" object. An instance of this class will be
// destroyed along with the exports object to which it is weakly bound.
v8::Persistent<v8::Object> exports_;
};

static void Method(const v8::FunctionCallbackInfo<v8::Value>& info) {
// Retrieve the per-addon-instance data.
AddonData* data =
reinterpret_cast<AddonData*>(info.Data().As<External>()->Value());
data->call_count++;
info.GetReturnValue().Set((double)data->call_count);
}

// Initialize this addon to be context-aware.
NODE_MODULE_INIT(/* exports, module, context */) {
Isolate* isolate = context->GetIsolate();

// Create a new instance of AddonData for this instance of the addon.
AddonData* data = new AddonData(isolate, exports);
// Wrap the data in a v8::External so we can pass it to the method we expose.
Local<External> external = External::New(isolate, data);

// Expose the method "Method" to JavaScript, and make sure it receives the
// per-addon-instance data we created above by passing `external` as the
// third parameter to the FunctionTemplate constructor.
exports->Set(context,
String::NewFromUtf8(isolate, "method", NewStringType::kNormal)
.ToLocalChecked(),
FunctionTemplate::New(isolate, Method, external)
->GetFunction(context).ToLocalChecked()).FromJust();
}
```
### Building
Once the source code has been written, it must be compiled into the binary
Expand Down
22 changes: 22 additions & 0 deletions src/node.h
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,28 @@ extern "C" NODE_EXTERN void node_module_register(void* mod);
*/
#define NODE_MODULE_DECL /* nothing */

#define NODE_MODULE_INITIALIZER_BASE node_register_module_v

#define NODE_MODULE_INITIALIZER_X(base, version) \
NODE_MODULE_INITIALIZER_X_HELPER(base, version)

#define NODE_MODULE_INITIALIZER_X_HELPER(base, version) base##version

#define NODE_MODULE_INITIALIZER \
NODE_MODULE_INITIALIZER_X(NODE_MODULE_INITIALIZER_BASE, \
NODE_MODULE_VERSION)

#define NODE_MODULE_INIT() \
extern "C" NODE_MODULE_EXPORT void \
NODE_MODULE_INITIALIZER(v8::Local<v8::Object> exports, \
v8::Local<v8::Value> module, \
v8::Local<v8::Context> context); \
NODE_MODULE_CONTEXT_AWARE(NODE_GYP_MODULE_NAME, \
NODE_MODULE_INITIALIZER) \
void NODE_MODULE_INITIALIZER(v8::Local<v8::Object> exports, \
v8::Local<v8::Value> module, \
v8::Local<v8::Context> context)

/* Called after the event loop exits but before the VM is disposed.
* Callbacks are run in reverse order of registration, i.e. newest first.
*/
Expand Down
13 changes: 6 additions & 7 deletions test/addons/hello-world/binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -6,13 +6,12 @@ void Method(const v8::FunctionCallbackInfo<v8::Value>& args) {
args.GetReturnValue().Set(v8::String::NewFromUtf8(isolate, "world"));
}

#define CONCAT(a, b) CONCAT_HELPER(a, b)
#define CONCAT_HELPER(a, b) a##b
#define INITIALIZER CONCAT(node_register_module_v, NODE_MODULE_VERSION)

extern "C" NODE_MODULE_EXPORT void INITIALIZER(v8::Local<v8::Object> exports,
v8::Local<v8::Value> module,
v8::Local<v8::Context> context) {
// Not using the full NODE_MODULE_INIT() macro here because we want to test the
// addon loader's reaction to the FakeInit() entry point below.
extern "C" NODE_MODULE_EXPORT void
NODE_MODULE_INITIALIZER(v8::Local<v8::Object> exports,
v8::Local<v8::Value> module,
v8::Local<v8::Context> context) {
NODE_SET_METHOD(exports, "hello", Method);
}

Expand Down

0 comments on commit e9a452b

Please sign in to comment.