From 89aa6b638ac93681360c13f9d257ad9f4c40064e 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 | 26 ++++++++++++++++++++++++++ napi.h | 11 +++++++++++ test/addon_data.cc | 34 ++++++++++++++++++++++++++++++++++ test/addon_data.js | 35 +++++++++++++++++++++++++++++++++++ test/binding.cc | 2 ++ test/binding.gyp | 1 + test/index.js | 1 + 7 files changed, 110 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 2855e80ef..924c00d16 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -322,6 +322,32 @@ inline Value Env::RunScript(String script) { return Value(_env, result); } +#ifdef NAPI_EXPERIMENTAL +template fini = Env::DefaultFini> +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_EXPERIMENTAL + //////////////////////////////////////////////////////////////////////////////// // Value class //////////////////////////////////////////////////////////////////////////////// diff --git a/napi.h b/napi.h index 49435d59b..a09fb91f9 100644 --- a/napi.h +++ b/napi.h @@ -173,6 +173,10 @@ namespace Napi { /// /// In the V8 JavaScript engine, a N-API environment approximately corresponds to an Isolate. class Env { +#ifdef NAPI_EXPERIMENTAL + private: + template static void DefaultFini(T* data); +#endif public: Env(napi_env env); @@ -189,6 +193,13 @@ namespace Napi { Value RunScript(const std::string& utf8script); Value RunScript(String script); +#ifdef NAPI_EXPERIMENTAL + template using Finalizer = void (*)(T*); + template fini = Env::DefaultFini> + void SetInstanceData(T* data); + template T* GetInstanceData(); +#endif + private: napi_env _env; }; diff --git a/test/addon_data.cc b/test/addon_data.cc new file mode 100644 index 000000000..449729350 --- /dev/null +++ b/test/addon_data.cc @@ -0,0 +1,34 @@ +#include +#define NAPI_EXPERIMENTAL +#include "napi.h" + +class Addon { + public: + Addon() {} + bool verbose = false; + ~Addon() { + if (verbose) { + fprintf(stderr, "addon_data: Addon::~Addon\n"); + } + } +}; + +static Napi::Value Getter(const Napi::CallbackInfo& info) { + return Napi::Boolean::New(info.Env(), + info.Env().GetInstanceData()->verbose); +} + +static void Setter(const Napi::CallbackInfo& info) { + info.Env().GetInstanceData()->verbose = info[0].As(); +} + +Napi::Object InitAddonData(Napi::Env env) { + env.SetInstanceData(new Addon()); + Napi::Object result = Napi::Object::New(env); + + result.DefineProperties({ + Napi::PropertyDescriptor::Accessor("verbose"), + }); + + return result; +} diff --git a/test/addon_data.js b/test/addon_data.js new file mode 100644 index 000000000..999242131 --- /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, false); + binding.addon_data.verbose = true; + assert.strictEqual(binding.addon_data.verbose, true); + binding.addon_data.verbose = false; + assert.strictEqual(binding.addon_data.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 aa9db6e41..95e524200 100644 --- a/test/binding.cc +++ b/test/binding.cc @@ -2,6 +2,7 @@ using namespace Napi; +Object InitAddonData(Env env); Object InitArrayBuffer(Env env); Object InitAsyncContext(Env env); #if (NAPI_VERSION > 3) @@ -57,6 +58,7 @@ Object InitVersionManagement(Env env); Object InitThunkingManual(Env env); Object Init(Env env, Object exports) { + exports.Set("addon_data", InitAddonData(env)); 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 b6777808d..3443de56c 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', 'asyncprogressworker.cc', diff --git a/test/index.js b/test/index.js index 1bd1d9144..2f331351e 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', 'asyncprogressworker',