Skip to content

Commit

Permalink
src: add support for addon instance data
Browse files Browse the repository at this point in the history
Support `napi_get_instance_data()` and `napi_set_instance_data()`.

Signed-off-by: Gabriel Schulhof <[email protected]>
Fixes: nodejs#654
PR-URL: nodejs#663
Reviewed-By: Chengzhong Wu <[email protected]>
Reviewed-By: Michael Dawson <[email protected]>
  • Loading branch information
Gabriel Schulhof committed Apr 25, 2020
1 parent 82a9650 commit 9c9accf
Show file tree
Hide file tree
Showing 7 changed files with 212 additions and 0 deletions.
42 changes: 42 additions & 0 deletions napi-inl.h
Original file line number Diff line number Diff line change
Expand Up @@ -322,6 +322,48 @@ inline Value Env::RunScript(String script) {
return Value(_env, result);
}

#if NAPI_VERSION > 5
template <typename T, Env::Finalizer<T> fini>
inline void Env::SetInstanceData(T* data) {
napi_status status =
napi_set_instance_data(_env, data, [](napi_env env, void* data, void*) {
fini(env, static_cast<T*>(data));
}, nullptr);
NAPI_THROW_IF_FAILED_VOID(_env, status);
}

template <typename DataType,
typename HintType,
Napi::Env::FinalizerWithHint<DataType, HintType> fini>
inline void Env::SetInstanceData(DataType* data, HintType* hint) {
napi_status status =
napi_set_instance_data(_env, data,
[](napi_env env, void* data, void* hint) {
fini(env, static_cast<DataType*>(data), static_cast<HintType*>(hint));
}, hint);
NAPI_THROW_IF_FAILED_VOID(_env, status);
}

template <typename T>
inline T* Env::GetInstanceData() {
void* data = nullptr;

napi_status status = napi_get_instance_data(_env, &data);
NAPI_THROW_IF_FAILED(_env, status, nullptr);

return static_cast<T*>(data);
}

template <typename T> void Env::DefaultFini(Env, T* data) {
delete data;
}

template <typename DataType, typename HintType>
void Env::DefaultFiniWithHint(Env, DataType* data, HintType*) {
delete data;
}
#endif // NAPI_VERSION > 5

////////////////////////////////////////////////////////////////////////////////
// Value class
////////////////////////////////////////////////////////////////////////////////
Expand Down
22 changes: 22 additions & 0 deletions napi.h
Original file line number Diff line number Diff line change
Expand Up @@ -166,6 +166,12 @@ namespace Napi {
///
/// In the V8 JavaScript engine, a N-API environment approximately corresponds to an Isolate.
class Env {
#if NAPI_VERSION > 5
private:
template <typename T> static void DefaultFini(Env, T* data);
template <typename DataType, typename HintType>
static void DefaultFiniWithHint(Env, DataType* data, HintType* hint);
#endif // NAPI_VERSION > 5
public:
Env(napi_env env);

Expand All @@ -182,6 +188,22 @@ namespace Napi {
Value RunScript(const std::string& utf8script);
Value RunScript(String script);

#if NAPI_VERSION > 5
template <typename T> T* GetInstanceData();

template <typename T> using Finalizer = void (*)(Env, T*);
template <typename T, Finalizer<T> fini = Env::DefaultFini<T>>
void SetInstanceData(T* data);

template <typename DataType, typename HintType>
using FinalizerWithHint = void (*)(Env, DataType*, HintType*);
template <typename DataType,
typename HintType,
FinalizerWithHint<DataType, HintType> fini =
Env::DefaultFiniWithHint<DataType, HintType>>
void SetInstanceData(DataType* data, HintType* hint);
#endif // NAPI_VERSION > 5

private:
napi_env _env;
};
Expand Down
97 changes: 97 additions & 0 deletions test/addon_data.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,97 @@
#if (NAPI_VERSION > 5)
#include <stdio.h>
#include "napi.h"

// An overly elaborate way to get/set a boolean stored in the instance data:
// 0. A boolean named "verbose" is stored in the instance data. The constructor
// for JS `VerboseIndicator` instances is also stored in the instance data.
// 1. Add a property named "verbose" onto exports served by a getter/setter.
// 2. The getter returns a object of type VerboseIndicator, which itself has a
// property named "verbose", also served by a getter/setter:
// * The getter returns a boolean, indicating whether "verbose" is set.
// * The setter sets "verbose" on the instance data.
// 3. The setter sets "verbose" on the instance data.

class Addon {
public:
class VerboseIndicator : public Napi::ObjectWrap<VerboseIndicator> {
public:
VerboseIndicator(const Napi::CallbackInfo& info):
Napi::ObjectWrap<VerboseIndicator>(info) {
info.This().As<Napi::Object>()["verbose"] =
Napi::Boolean::New(info.Env(),
info.Env().GetInstanceData<Addon>()->verbose);
}

Napi::Value Getter(const Napi::CallbackInfo& info) {
return Napi::Boolean::New(info.Env(),
info.Env().GetInstanceData<Addon>()->verbose);
}

void Setter(const Napi::CallbackInfo& info, const Napi::Value& val) {
info.Env().GetInstanceData<Addon>()->verbose = val.As<Napi::Boolean>();
}

static Napi::FunctionReference Init(Napi::Env env) {
return Napi::Persistent(DefineClass(env, "VerboseIndicator", {
InstanceAccessor<
&VerboseIndicator::Getter,
&VerboseIndicator::Setter>("verbose")
}));
}
};

static Napi::Value Getter(const Napi::CallbackInfo& info) {
return info.Env().GetInstanceData<Addon>()->VerboseIndicator.New({});
}

static void Setter(const Napi::CallbackInfo& info) {
info.Env().GetInstanceData<Addon>()->verbose = info[0].As<Napi::Boolean>();
}

Addon(Napi::Env env): VerboseIndicator(VerboseIndicator::Init(env)) {}
~Addon() {
if (verbose) {
fprintf(stderr, "addon_data: Addon::~Addon\n");
}
}

static void DeleteAddon(Napi::Env, Addon* addon, uint32_t* hint) {
delete addon;
fprintf(stderr, "hint: %d\n", *hint);
delete hint;
}

static Napi::Object Init(Napi::Env env, Napi::Value jshint) {
if (!jshint.IsNumber()) {
NAPI_THROW(Napi::Error::New(env, "Expected number"), Napi::Object());
}
uint32_t hint = jshint.As<Napi::Number>();
if (hint == 0)
env.SetInstanceData(new Addon(env));
else
env.SetInstanceData<Addon, uint32_t, DeleteAddon>(new Addon(env),
new uint32_t(hint));
Napi::Object result = Napi::Object::New(env);
result.DefineProperties({
Napi::PropertyDescriptor::Accessor<Getter, Setter>("verbose"),
});

return result;
}

private:
bool verbose = false;
Napi::FunctionReference VerboseIndicator;
};

// We use an addon factory so we can cover both the case where there is an
// instance data hint and the case where there isn't.
static Napi::Value AddonFactory(const Napi::CallbackInfo& info) {
return Addon::Init(info.Env(), info[0]);
}

Napi::Object InitAddonData(Napi::Env env) {
return Napi::Function::New(env, AddonFactory);
}
#endif // (NAPI_VERSION > 5)
42 changes: 42 additions & 0 deletions test/addon_data.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
'use strict';
const buildType = process.config.target_defaults.default_configuration;
const assert = require('assert');
const { spawn } = require('child_process');
const readline = require('readline');
const path = require('path');

test(path.resolve(__dirname, `./build/${buildType}/binding.node`));
test(path.resolve(__dirname, `./build/${buildType}/binding_noexcept.node`));

// Make sure the instance data finalizer is called at process exit. If the hint
// is non-zero, it will be printed out by the child process.
function testFinalizer(bindingName, hint, expected) {
bindingName = bindingName.split('\\').join('\\\\');
const child = spawn(process.execPath, [
'-e',
`require('${bindingName}').addon_data(${hint}).verbose = true;`
]);
const actual = [];
readline
.createInterface({ input: child.stderr })
.on('line', (line) => {
if (expected.indexOf(line) >= 0) {
actual.push(line);
}
})
.on('close', () => assert.deepStrictEqual(expected, actual));
}

function test(bindingName) {
const binding = require(bindingName).addon_data(0);

// Make sure it is possible to get/set instance data.
assert.strictEqual(binding.verbose.verbose, false);
binding.verbose = true;
assert.strictEqual(binding.verbose.verbose, true);
binding.verbose = false;
assert.strictEqual(binding.verbose.verbose, false);

testFinalizer(bindingName, 0, ['addon_data: Addon::~Addon']);
testFinalizer(bindingName, 42, ['addon_data: Addon::~Addon', 'hint: 42']);
}
6 changes: 6 additions & 0 deletions test/binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

using namespace Napi;

#if (NAPI_VERSION > 5)
Object InitAddonData(Env env);
#endif
Object InitArrayBuffer(Env env);
Object InitAsyncContext(Env env);
#if (NAPI_VERSION > 3)
Expand Down Expand Up @@ -55,6 +58,9 @@ Object InitVersionManagement(Env env);
Object InitThunkingManual(Env env);

Object Init(Env env, Object exports) {
#if (NAPI_VERSION > 5)
exports.Set("addon_data", InitAddonData(env));
#endif
exports.Set("arraybuffer", InitArrayBuffer(env));
exports.Set("asynccontext", InitAsyncContext(env));
#if (NAPI_VERSION > 3)
Expand Down
1 change: 1 addition & 0 deletions test/binding.gyp
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
'target_defaults': {
'includes': ['../common.gypi'],
'sources': [
'addon_data.cc',
'arraybuffer.cc',
'asynccontext.cc',
'asyncprogressqueueworker.cc',
Expand Down
2 changes: 2 additions & 0 deletions test/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ process.config.target_defaults.default_configuration =
// FIXME: We might need a way to load test modules automatically without
// explicit declaration as follows.
let testModules = [
'addon_data',
'arraybuffer',
'asynccontext',
'asyncprogressqueueworker',
Expand Down Expand Up @@ -81,6 +82,7 @@ if (napiVersion < 5) {
if (napiVersion < 6) {
testModules.splice(testModules.indexOf('bigint'), 1);
testModules.splice(testModules.indexOf('typedarray-bigint'), 1);
testModules.splice(testModules.indexOf('addon_data'), 1);
}

if (typeof global.gc === 'function') {
Expand Down

0 comments on commit 9c9accf

Please sign in to comment.