From a8959b679c8fffde7f6638ac5843be714bb196cc Mon Sep 17 00:00:00 2001 From: Gabriel Schulhof Date: Thu, 30 Jan 2020 18:54:24 -0800 Subject: [PATCH] src: add support for addon instance data Support `napi_get_instance_data()` and `napi_set_instance_data()`. Fixes: https://github.com/nodejs/node-addon-api/issues/654 --- napi-inl.h | 42 ++++++++++++++++++++ napi.h | 22 +++++++++++ test/addon_data.cc | 97 ++++++++++++++++++++++++++++++++++++++++++++++ test/addon_data.js | 42 ++++++++++++++++++++ test/binding.cc | 6 +++ test/binding.gyp | 1 + test/index.js | 2 + 7 files changed, 212 insertions(+) create mode 100644 test/addon_data.cc create mode 100644 test/addon_data.js diff --git a/napi-inl.h b/napi-inl.h index b3db7e918..7e4d158db 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -322,6 +322,48 @@ inline Value Env::RunScript(String script) { return Value(_env, result); } +#if NAPI_VERSION > 5 +template 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(data)); + }, nullptr); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +template 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(data), static_cast(hint)); + }, hint); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +template +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(data); +} + +template void Env::DefaultFini(Env, T* data) { + delete data; +} + +template +void Env::DefaultFiniWithHint(Env, DataType* data, HintType*) { + delete data; +} +#endif // NAPI_VERSION > 5 + //////////////////////////////////////////////////////////////////////////////// // Value class //////////////////////////////////////////////////////////////////////////////// diff --git a/napi.h b/napi.h index 7828a2b3f..de4d82f36 100644 --- a/napi.h +++ b/napi.h @@ -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 static void DefaultFini(Env, T* data); + template + static void DefaultFiniWithHint(Env, DataType* data, HintType* hint); +#endif // NAPI_VERSION > 5 public: Env(napi_env env); @@ -182,6 +188,22 @@ namespace Napi { Value RunScript(const std::string& utf8script); Value RunScript(String script); +#if NAPI_VERSION > 5 + template T* GetInstanceData(); + + template using Finalizer = void (*)(Env, T*); + template fini = Env::DefaultFini> + void SetInstanceData(T* data); + + template + using FinalizerWithHint = void (*)(Env, DataType*, HintType*); + template fini = + Env::DefaultFiniWithHint> + void SetInstanceData(DataType* data, HintType* hint); +#endif // NAPI_VERSION > 5 + private: napi_env _env; }; diff --git a/test/addon_data.cc b/test/addon_data.cc new file mode 100644 index 000000000..d160a5946 --- /dev/null +++ b/test/addon_data.cc @@ -0,0 +1,97 @@ +#if (NAPI_VERSION > 5) +#include +#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 { + public: + VerboseIndicator(const Napi::CallbackInfo& info): + Napi::ObjectWrap(info) { + info.This().As()["verbose"] = + Napi::Boolean::New(info.Env(), + info.Env().GetInstanceData()->verbose); + } + + Napi::Value Getter(const Napi::CallbackInfo& info) { + return Napi::Boolean::New(info.Env(), + info.Env().GetInstanceData()->verbose); + } + + void Setter(const Napi::CallbackInfo& info, const Napi::Value& val) { + info.Env().GetInstanceData()->verbose = val.As(); + } + + 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()->VerboseIndicator.New({}); + } + + static void Setter(const Napi::CallbackInfo& info) { + info.Env().GetInstanceData()->verbose = info[0].As(); + } + + 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(); + if (hint == 0) + env.SetInstanceData(new Addon(env)); + else + env.SetInstanceData(new Addon(env), + new uint32_t(hint)); + Napi::Object result = Napi::Object::New(env); + result.DefineProperties({ + Napi::PropertyDescriptor::Accessor("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) diff --git a/test/addon_data.js b/test/addon_data.js new file mode 100644 index 000000000..0a3852696 --- /dev/null +++ b/test/addon_data.js @@ -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']); +} diff --git a/test/binding.cc b/test/binding.cc index 8732ae7bd..ea1094638 100644 --- a/test/binding.cc +++ b/test/binding.cc @@ -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) @@ -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) diff --git a/test/binding.gyp b/test/binding.gyp index 2d6ac9549..8dafe13fc 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -2,6 +2,7 @@ 'target_defaults': { 'includes': ['../common.gypi'], 'sources': [ + 'addon_data.cc', 'arraybuffer.cc', 'asynccontext.cc', 'asyncprogressqueueworker.cc', diff --git a/test/index.js b/test/index.js index 0fc38d280..5fc752e3a 100644 --- a/test/index.js +++ b/test/index.js @@ -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', @@ -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') {