Skip to content

Commit ed56516

Browse files
author
Gabriel Schulhof
committed
Add example showing multiple addon load
PR-URL: #69 Reviewed-By: Michael Dawson <michael_dawson@ca.ibm.com>
1 parent d03c3d4 commit ed56516

8 files changed

Lines changed: 383 additions & 0 deletions

File tree

multiple_load/napi/binding.gyp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
'targets': [
3+
{
4+
'target_name': 'multiple_load',
5+
'sources': [ 'multiple_load.c' ]
6+
}
7+
]
8+
}

multiple_load/napi/index.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Example illustrating the case where a native addon is loaded multiple times.
2+
// This entire file is executed twice, concurrently - once on the main thread,
3+
// and once on a thread launched from the main thread.
4+
5+
// We load the worker threads module, which allows us to launch multiple Node.js
6+
// environments, each in its own thread.
7+
const {
8+
Worker, isMainThread
9+
} = require('worker_threads');
10+
11+
// We load the native addon.
12+
const addon = require('bindings')('multiple_load');
13+
14+
// The iteration count can be tweaked to ensure that the output from the two
15+
// threads is interleaved. Too few iterations and the output of one thread
16+
// follows the output of the other, not really illustrating the concurrency.
17+
const iterations = 1000;
18+
19+
// This function is an idle loop that performs a random walk from 0 by calling
20+
// into the native addon to either increment or decrement the initial value.
21+
function useAddon(addon, prefix, iterations) {
22+
if (iterations >= 0) {
23+
if (Math.random() < 0.5) {
24+
console.log(prefix + ': new value (decremented): ' + addon.decrement());
25+
} else {
26+
console.log(prefix + ': new value (incremented): ' + addon.increment());
27+
}
28+
setImmediate(() => useAddon(addon, prefix, --iterations));
29+
}
30+
}
31+
32+
if (isMainThread) {
33+
// On the main thread, we launch a worker and wait for it to come online. Then
34+
// we start the loop.
35+
(new Worker(__filename)).on('online',
36+
() => useAddon(addon, "Main thread", iterations));
37+
} else {
38+
// On the secondary thread we immediately start the loop.
39+
useAddon(addon, "Worker thread", iterations);
40+
}

multiple_load/napi/multiple_load.c

Lines changed: 136 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,136 @@
1+
#include <assert.h>
2+
#include <math.h>
3+
#include <stdlib.h>
4+
#include <node_api.h>
5+
6+
// Structure containing information needed for as long as the addon exists. It
7+
// replaces the use of global static data with per-addon-instance data by
8+
// associating an instance of this structure with each instance of this addon
9+
// during addon initialization. The instance of this structure is then passed to
10+
// each binding the addon provides. Thus, the data stored in an instance of this
11+
// structure is available to each binding, just as global static data would be.
12+
typedef struct {
13+
double value;
14+
} AddonData;
15+
16+
// This is the actual, useful work performed: increment or decrement the value
17+
// stored per addon instance after passing it through a CPU-consuming but
18+
// otherwise useless calculation.
19+
static int ModifyAddonData(AddonData* data, double offset) {
20+
// Expensively increment or decrement the value.
21+
data->value = tan(atan(exp(log(sqrt(data->value * data->value))))) + offset;
22+
23+
// Round the value to the nearest integer.
24+
data->value =
25+
(double)(((int)data->value) +
26+
(data->value - ((double)(int)data->value) > 0.5 ? 1 : 0));
27+
28+
// Return the value as an integer.
29+
return (int)(data->value);
30+
}
31+
32+
// This is boilerplate. The instance of the `AddonData` structure created during
33+
// addon initialization must be destroyed when the addon is unloaded. This
34+
// function will be called when the addon's `exports` object is garbage collected.
35+
static void DeleteAddonData(napi_env env, void* data, void* hint) {
36+
// Avoid unused parameter warnings.
37+
(void) env;
38+
(void) hint;
39+
40+
// Free the per-addon-instance data.
41+
free(data);
42+
}
43+
44+
// This is also boilerplate. It creates and initializes an instance of the
45+
// `AddonData` structure and ties its lifecycle to that of the addon instance's
46+
// `exports` object. This means that the data will be available to this instance
47+
// of the addon for as long as the JavaScript engine keeps it alive.
48+
static AddonData* CreateAddonData(napi_env env, napi_value exports) {
49+
AddonData* result = malloc(sizeof(*result));
50+
result->value = 0.0;
51+
assert(napi_wrap(env,
52+
exports,
53+
result,
54+
DeleteAddonData,
55+
NULL,
56+
NULL) == napi_ok);
57+
return result;
58+
}
59+
60+
// This function is called from JavaScript. It uses an expensive operation to
61+
// increment the value stored inside the `AddonData` structure by one.
62+
static napi_value Increment(napi_env env, napi_callback_info info) {
63+
// Retrieve the per-addon-instance data.
64+
AddonData* addon_data = NULL;
65+
assert(napi_get_cb_info(env,
66+
info,
67+
NULL,
68+
NULL,
69+
NULL,
70+
((void**)&addon_data)) == napi_ok);
71+
72+
// Increment the per-addon-instance value and create a new JavaScript integer
73+
// from it.
74+
napi_value result;
75+
assert(napi_create_int32(env,
76+
ModifyAddonData(addon_data, 1.0),
77+
&result) == napi_ok);
78+
79+
// Return the JavaScript integer back to JavaScript.
80+
return result;
81+
}
82+
83+
// This function is called from JavaScript. It uses an expensive operation to
84+
// decrement the value stored inside the `AddonData` structure by one.
85+
static napi_value Decrement(napi_env env, napi_callback_info info) {
86+
// Retrieve the per-addon-instance data.
87+
AddonData* addon_data = NULL;
88+
assert(napi_get_cb_info(env,
89+
info,
90+
NULL,
91+
NULL,
92+
NULL,
93+
((void**)&addon_data)) == napi_ok);
94+
95+
// Decrement the per-addon-instance value and create a new JavaScript integer
96+
// from it.
97+
napi_value result;
98+
assert(napi_create_int32(env,
99+
ModifyAddonData(addon_data, -1.0),
100+
&result) == napi_ok);
101+
102+
// Return the JavaScript integer back to JavaScript.
103+
return result;
104+
}
105+
106+
// Initialize the addon in such a way that it may be initialized multiple times
107+
// per process. The function body following this macro is provided the value
108+
// `env` which has type `napi_env` and the value `exports` which has type
109+
// `napi_value` and which refers to a JavaScript object that ultimately contains
110+
// the functions this addon wishes to expose. At the end, it must return a
111+
// `napi_value`. It may return `exports`, or it may create a new `napi_value`
112+
// and return that instead.
113+
NAPI_MODULE_INIT(/*env, exports*/) {
114+
// Create a new instance of the per-instance-data that will be associated with
115+
// the instance of the addon being initialized here and that will be destroyed
116+
// along with the instance of the addon.
117+
AddonData* addon_data = CreateAddonData(env, exports);
118+
119+
// Declare the bindings this addon provides. The data created above is given
120+
// as the last initializer parameter, and will be given to the binding when it
121+
// is called.
122+
napi_property_descriptor bindings[] = {
123+
{"increment", NULL, Increment, NULL, NULL, NULL, napi_enumerable, addon_data},
124+
{"decrement", NULL, Decrement, NULL, NULL, NULL, napi_enumerable, addon_data}
125+
};
126+
127+
// Expose the two bindings declared above to JavaScript.
128+
assert(napi_define_properties(env,
129+
exports,
130+
sizeof(bindings) / sizeof(bindings[0]),
131+
bindings) == napi_ok);
132+
133+
// Return the `exports` object provided. It now has two new properties, which
134+
// are the functions we wish to expose to JavaScript.
135+
return exports;
136+
}

multiple_load/napi/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "multiple_load",
3+
"version": "0.0.0",
4+
"description": "Multiple load example",
5+
"main": "index.js",
6+
"private": true,
7+
"scripts": {
8+
"test": "node --experimental-worker index.js"
9+
},
10+
"engines": {
11+
"node": ">= 10.10.0"
12+
},
13+
"gypfile": true,
14+
"dependencies": {
15+
"bindings": "~1.2.1"
16+
}
17+
}

multiple_load/node_10/binding.gyp

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
{
2+
'targets': [
3+
{
4+
'target_name': 'multiple_load',
5+
'sources': [ 'multiple_load.cc' ]
6+
}
7+
]
8+
}

multiple_load/node_10/index.js

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
// Example illustrating the case where a native addon is loaded multiple times.
2+
// This entire file is executed twice, concurrently - once on the main thread,
3+
// and once on a thread launched from the main thread.
4+
5+
// We load the worker threads module, which allows us to launch multiple Node.js
6+
// environments, each in its own thread.
7+
const {
8+
Worker, isMainThread
9+
} = require('worker_threads');
10+
11+
// We load the native addon.
12+
const addon = require('bindings')('multiple_load');
13+
14+
// The iteration count can be tweaked to ensure that the output from the two
15+
// threads is interleaved. Too few iterations and the output of one thread
16+
// follows the output of the other, not really illustrating the concurrency.
17+
const iterations = 1000;
18+
19+
// This function is an idle loop that performs a random walk from 0 by calling
20+
// into the native addon to either increment or decrement the initial value.
21+
function useAddon(addon, prefix, iterations) {
22+
if (iterations >= 0) {
23+
if (Math.random() < 0.5) {
24+
console.log(prefix + ': new value (decremented): ' + addon.decrement());
25+
} else {
26+
console.log(prefix + ': new value (incremented): ' + addon.increment());
27+
}
28+
setImmediate(() => useAddon(addon, prefix, --iterations));
29+
}
30+
}
31+
32+
if (isMainThread) {
33+
// On the main thread, we launch a worker and wait for it to come online. Then
34+
// we start the loop.
35+
(new Worker(__filename)).on('online',
36+
() => useAddon(addon, "Main thread", iterations));
37+
} else {
38+
// On the secondary thread we immediately start the loop.
39+
useAddon(addon, "Worker thread", iterations);
40+
}
Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
#include <node.h>
2+
#include <math.h>
3+
4+
using namespace v8;
5+
6+
// Class containing information needed for as long as the addon exists. It
7+
// replaces the use of global static data with per-addon-instance data by
8+
// associating an instance of this class with each instance of this addon during
9+
// addon initialization. The instance of this class is then passed to each
10+
// binding the addon provides. Thus, the data stored in an instance of this
11+
// class is available to each binding, just as global static data would be.
12+
class AddonData {
13+
public:
14+
// This is the actual, useful work performed: increment or decrement the value
15+
// stored per addon instance after passing it through a CPU-consuming but
16+
// otherwise useless calculation. Round the result to the nearest integer.
17+
int GetNewValue(double offset) {
18+
// Expensively increment or decrement the value.
19+
value = tan(atan(exp(log(sqrt(value * value))))) + offset;
20+
21+
// Round the value to the nearest integer.
22+
value =
23+
(double)(((int)value) + (value - ((double)(int)value) > 0.5 ? 1 : 0));
24+
25+
// Return the value as an integer.
26+
return (int)value;
27+
}
28+
29+
// This is boilerplate. It creates a new instance of this class and wraps it
30+
// into a v8::External. The isolate and the exports are necessary, because we
31+
// want the instance of this class to be destroyed along with the exports
32+
// object when the addon is eventually unloaded.
33+
static Local<Value> New(Isolate* isolate, Local<Object> exports) {
34+
return External::New(isolate, new AddonData(isolate, exports));
35+
}
36+
37+
private:
38+
// This is the actual, useful payload carried by an instance of this class.
39+
// A double value is kept around for as long as an instance of this addon is
40+
// loaded, and is incremented or decremented whenever the addon receives a
41+
// call from JavaScript.
42+
double value;
43+
44+
explicit AddonData(Isolate* isolate, Local<Object> exports):
45+
// The payload is initialized here. The rest of the constructor is
46+
// boilerplate that ensures that the instance of this addon data is
47+
// destroyed along with the instance of this addon (`exports`).
48+
value(0.0) {
49+
exports_persistent.Reset(isolate, exports);
50+
exports_persistent.SetWeak(this, DeleteMe, WeakCallbackType::kParameter);
51+
}
52+
53+
// The rest of the class definition is boilerplate.
54+
55+
// The persistent reference must be reset before the instance of this class is
56+
// destroyed, otherwise memory will leak on the V8 side.
57+
~AddonData() {
58+
exports_persistent.Reset();
59+
}
60+
61+
// This static function will be called when the addon instance is unloaded. It
62+
// merely destroys the per-addon-instance data.
63+
static void DeleteMe(const WeakCallbackInfo<AddonData>& info) {
64+
delete info.GetParameter();
65+
}
66+
67+
Persistent<Object> exports_persistent;
68+
};
69+
70+
// Function called from JavaScript to increment the value stored in this addon.
71+
void Increment(const FunctionCallbackInfo<Value>& info) {
72+
// Retrieve the per-addon-instance data.
73+
AddonData* addon_data =
74+
static_cast<AddonData*>(info.Data().As<External>()->Value());
75+
76+
info.GetReturnValue()
77+
.Set(Number::New(info.GetIsolate(), addon_data->GetNewValue(1.0)));
78+
}
79+
80+
// Function called from JavaScript to decrement the value stored in this addon.
81+
void Decrement(const FunctionCallbackInfo<Value>& info) {
82+
// Retrieve the per-addon-instance data.
83+
AddonData* addon_data =
84+
static_cast<AddonData*>(info.Data().As<External>()->Value());
85+
86+
info.GetReturnValue()
87+
.Set(Number::New(info.GetIsolate(), addon_data->GetNewValue(-1.0)));
88+
}
89+
90+
// Initialize the addon in such a way that it may be initialized multiple times
91+
// per process. The function body following this macro is provided the value
92+
// `exports` of type `Local<Object>`, the value `module` of type
93+
// `Local<Object>`, and `context` of type `Local<Context>`. It may either define
94+
// new properties on the `exports` object, or define the property named
95+
// "exports" on the `module` object.
96+
NODE_MODULE_INIT(/*exports, module, context*/) {
97+
Isolate* isolate = context->GetIsolate();
98+
99+
// Create a new instance of the addon data that will be associated with this
100+
// instance of the addon, and that will be freed along with this instance of
101+
// the addon.
102+
Local<Value> addon_data = AddonData::New(isolate, exports);
103+
104+
// Export the functions we wish to make available to JavaScript.
105+
106+
exports->Set(context,
107+
String::NewFromUtf8(isolate, "increment", NewStringType::kNormal)
108+
.ToLocalChecked(),
109+
FunctionTemplate::New(isolate, Increment, addon_data)
110+
->GetFunction(context).ToLocalChecked()).FromJust();
111+
112+
exports->Set(context,
113+
String::NewFromUtf8(isolate, "decrement", NewStringType::kNormal)
114+
.ToLocalChecked(),
115+
FunctionTemplate::New(isolate, Decrement, addon_data)
116+
->GetFunction(context).ToLocalChecked()).FromJust();
117+
}

multiple_load/node_10/package.json

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
{
2+
"name": "multiple_load",
3+
"version": "0.0.0",
4+
"description": "Multiple load example",
5+
"main": "index.js",
6+
"private": true,
7+
"scripts": {
8+
"test": "node --experimental-worker index.js"
9+
},
10+
"engines": {
11+
"node": ">= 10.10.0"
12+
},
13+
"gypfile": true,
14+
"dependencies": {
15+
"bindings": "~1.2.1"
16+
}
17+
}

0 commit comments

Comments
 (0)