From 7db12f7bad3eb92621885668bc618df94a3f72ba 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 | 25 +++++++++++++++ napi.h | 11 +++++++ test/addon_data.cc | 77 ++++++++++++++++++++++++++++++++++++++++++++++ test/addon_data.js | 35 +++++++++++++++++++++ test/binding.cc | 6 ++++ test/binding.gyp | 1 + test/index.js | 5 +++ 7 files changed, 160 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 f8657ae3f..2b03e5ba8 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -322,6 +322,31 @@ 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, void* data, void*) { + fini(static_cast(data)); + }, nullptr); + 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(T* data) { + delete data; +} +#endif // NAPI_VERSION > 5 + //////////////////////////////////////////////////////////////////////////////// // Value class //////////////////////////////////////////////////////////////////////////////// diff --git a/napi.h b/napi.h index e4b964f87..c14c7f301 100644 --- a/napi.h +++ b/napi.h @@ -172,6 +172,10 @@ 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(T* data); +#endif // NAPI_VERSION > 5 public: Env(napi_env env); @@ -188,6 +192,13 @@ namespace Napi { Value RunScript(const std::string& utf8script); Value RunScript(String script); +#if NAPI_VERSION > 5 + template using Finalizer = void (*)(T*); + template fini = Env::DefaultFini> + void SetInstanceData(T* data); + template T* GetInstanceData(); +#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..021f23f90 --- /dev/null +++ b/test/addon_data.cc @@ -0,0 +1,77 @@ +#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 Napi::Object Init(Napi::Env env) { + env.SetInstanceData(new Addon(env)); + Napi::Object result = Napi::Object::New(env); + result.DefineProperties({ + Napi::PropertyDescriptor::Accessor("verbose"), + }); + + return result; + } + + private: + bool verbose = false; + Napi::FunctionReference VerboseIndicator; +}; + +Napi::Object InitAddonData(Napi::Env env) { + return Addon::Init(env); +} +#endif // (NAPI_VERSION > 5) diff --git a/test/addon_data.js b/test/addon_data.js new file mode 100644 index 000000000..24edcbb03 --- /dev/null +++ b/test/addon_data.js @@ -0,0 +1,35 @@ +'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`)); + +function test(bindingName) { + const binding = require(bindingName); + + // Make sure it is possible to get/set instance data. + assert.strictEqual(binding.addon_data.verbose.verbose, false); + binding.addon_data.verbose = true; + assert.strictEqual(binding.addon_data.verbose.verbose, true); + binding.addon_data.verbose = false; + assert.strictEqual(binding.addon_data.verbose.verbose, false); + + // Make sure the instance data finalizer is called at process exit. + const child = spawn(process.execPath, [ + '-e', + `require('${bindingName}').addon_data.verbose = true;` + ]); + let foundMessage = false; + readline + .createInterface({ input: child.stderr }) + .on('line', (line) => { + if (line.match('addon_data: Addon::~Addon')) { + foundMessage = true; + } + }) + .on('close', () => assert.strictEqual(foundMessage, true)); +} diff --git a/test/binding.cc b/test/binding.cc index 111bcce01..54c28ade6 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) @@ -58,6 +61,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 e96ac5bf8..2a20fe737 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', @@ -87,6 +88,10 @@ if (napiVersion < 5) { testModules.splice(testModules.indexOf('date'), 1); } +if (napiVersion < 6) { + testModules.splice(testModules.indexOf('addon_data'), 1); +} + if (typeof global.gc === 'function') { console.log(`Testing with N-API Version '${napiVersion}'.`); console.log(`Testing with Node.js Major Version '${nodeMajorVersion}'.\n`);