From 3e4e1a3c8c42eb3f4dcf447ddb5273ab8f3829e1 Mon Sep 17 00:00:00 2001 From: Michael Dawson Date: Thu, 26 Apr 2018 17:47:45 -0400 Subject: [PATCH] test: add basic tests and doc for scopes --- README.md | 2 +- doc/escapable_handle_scope.md | 82 ++++++++++++++++++++++++++++++ doc/escapable_handle_sope.md | 5 -- doc/handle_scope.md | 69 +++++++++++++++++++++++-- doc/object_lifetime_management.md | 84 +++++++++++++++++++++++++++++++ doc/onject_lifetime_management.md | 5 -- test/binding.cc | 2 + test/binding.gyp | 1 + test/handlescope.cc | 56 +++++++++++++++++++++ test/handlescope.js | 15 ++++++ test/index.js | 1 + 11 files changed, 307 insertions(+), 15 deletions(-) create mode 100644 doc/escapable_handle_scope.md delete mode 100644 doc/escapable_handle_sope.md create mode 100644 doc/object_lifetime_management.md delete mode 100644 doc/onject_lifetime_management.md create mode 100644 test/handlescope.cc create mode 100644 test/handlescope.js diff --git a/README.md b/README.md index ebf6fec2d..2516ec0b5 100644 --- a/README.md +++ b/README.md @@ -64,7 +64,7 @@ values. Concepts and operations generally map to ideas specified in the - [PropertyDescriptor](doc/property_descriptor.md) - [Error Handling](doc/error_handling.md) - [Error](doc/error.md) - - [Object Lifettime Management](doc/object_lifetime_management.md) + - [Object Lifetime Management](doc/object_lifetime_management.md) - [HandleScope](doc/handle_scope.md) - [EscapableHandleScope](doc/escapable_handle_scope.md) - [Working with JavaScript Values](doc/working_with_javascript_values.md) diff --git a/doc/escapable_handle_scope.md b/doc/escapable_handle_scope.md new file mode 100644 index 000000000..1bf15879a --- /dev/null +++ b/doc/escapable_handle_scope.md @@ -0,0 +1,82 @@ +# EscapableHandleScope + +The EscapableHandleScope class is used to manage the lifetime of object handles +which are created through the use of node-addon-api. These handles +keep an object alive in the heap in order to ensure that the objects +are not collected by the garbage collector while native code is using them. +A handle may be created when any new node-addon-api Value or one +of its subclasses is created or returned. + +An EscapableHandleScope is a special type of HandleScope +which allows a single handle to be "promoted" to an outer scope. + +For more details refer to the section titled +(Object lifetime management)[object_lifetime_management]. + +## Methods + +### Constructor + +Creates a new escapable handle scope. + +```cpp +EscapableHandleScope EscapableHandleScope::New(Napi:Env env); +``` + +- `[in] Env`: The environment in which to construct the EscapableHandleScope object. + +Returns a new EscapableHandleScope + +### Constructor + +Creates a new escapable handle scope. + +```cpp +EscapableHandleScope EscapableHandleScope::New(napi_env env, napi_handle_scope scope); +``` + +- `[in] env`: napi_env in which the scope passed in was created. +- `[in] scope`: pre-existing napi_handle_scope. + +Returns a new EscapableHandleScope instance which wraps the +napi_escapable_handle_scope handle passed in. This can be used +to mix usage of the C N-API and node-addon-api. + +operator EscapableHandleScope::napi_escapable_handle_scope + +```cpp +operator EscapableHandleScope::napi_escapable_handle_scope() const +``` + +Returns the N-API napi_escapable_handle_scope wrapped by the EscapableHandleScope object. +This can be used to mix usage of the C N-API and node-addon-api by allowing +the class to be used be converted to a napi_escapable_handle_scope. + +### Destructor +```cpp +~EscapableHandleScope(); +``` + +Deletes the EscapableHandleScope instance and allows any objects/handles created +in the scope to be collected by the garbage collector. There is no +guarantee as to when the gargbage collector will do this. + +### Escape + +```cpp +napi::Value EscapableHandleScope::Escape(napi_value escapee); +``` + +- `[in] escapee`: Napi::Value or napi_env to promote to the outer scope + +Returns Napi:Value which can be used in the outer scope. This method can +be called at most once on a given EscapableHandleScope. If it is called +more than once an exception will be thrown. + +### Env + +```cpp +Napi::Env Env() const; +``` + +Returns the Napi:Env associated with the EscapableHandleScope. diff --git a/doc/escapable_handle_sope.md b/doc/escapable_handle_sope.md deleted file mode 100644 index 37d6ddd66..000000000 --- a/doc/escapable_handle_sope.md +++ /dev/null @@ -1,5 +0,0 @@ -# Escapable handle scope - -You are reading a draft of the next documentation and it's in continuos update so -if you don't find what you need please refer to: -[C++ wrapper classes for the ABI-stable C APIs for Node.js](https://nodejs.github.io/node-addon-api/) \ No newline at end of file diff --git a/doc/handle_scope.md b/doc/handle_scope.md index 2bf03becc..40342ba5d 100644 --- a/doc/handle_scope.md +++ b/doc/handle_scope.md @@ -1,5 +1,66 @@ -# Handle scope +# HandleScope -You are reading a draft of the next documentation and it's in continuos update so -if you don't find what you need please refer to: -[C++ wrapper classes for the ABI-stable C APIs for Node.js](https://nodejs.github.io/node-addon-api/) \ No newline at end of file +The HandleScope class is used to manage the lifetime of object handles +which are created through the use of node-addon-api. These handles +keep an object alive in the heap in order to ensure that the objects +are not collected while native code is using them. +A handle may be created when any new node-addon-api Value or one +of its subclasses is created or returned. For more details refer to +the section titled (Object lifetime management)[object_lifetime_management]. + +## Methods + +### Constructor + +Creates a new handle scope. + +```cpp +HandleScope HandleScope::New(Napi:Env env); +``` + +- `[in] Env`: The environment in which to construct the HandleScope object. + +Returns a new HandleScope + + +### Constructor + +Creates a new handle scope. + +```cpp +HandleScope HandleScope::New(napi_env env, napi_handle_scope scope); +``` + +- `[in] env`: napi_env in which the scope passed in was created. +- `[in] scope`: pre-existing napi_handle_scope. + +Returns a new HandleScope instance which wraps the napi_handle_scope +handle passed in. This can be used to mix usage of the C N-API +and node-addon-api. + +operator HandleScope::napi_handle_scope + +```cpp +operator HandleScope::napi_handle_scope() const +``` + +Returns the N-API napi_handle_scope wrapped by the EscapableHandleScope object. +This can be used to mix usage of the C N-API and node-addon-api by allowing +the class to be used be converted to a napi_handle_scope. + +### Destructor +```cpp +~HandleScope(); +``` + +Deletes the HandleScope instance and allows any objects/handles created +in the scope to be collected by the garbage collector. There is no +guarantee as to when the gargbage collector will do this. + +### Env + +```cpp +Napi::Env Env() const; +``` + +Returns the Napi:Env associated with the HandleScope. diff --git a/doc/object_lifetime_management.md b/doc/object_lifetime_management.md new file mode 100644 index 000000000..d292556cf --- /dev/null +++ b/doc/object_lifetime_management.md @@ -0,0 +1,84 @@ +# Object lifetime management + +A handle may be created when any new node-addon-api Value and +its subclasses is created or returned. + +As the methods and classes within the node-addon-api are used, +handles to objects in the heap for the underlying +VM may be created. A handle may be created when any new +node-addon-api Value or one of its subclasses is created or returned. +These handles must hold the objects 'live' until they are no +longer required by the native code, otherwise the objects could be +collected by the garbage collector before the native code was +finished using them. + +As handles are created they are associated with a +'scope'. The lifespan for the default scope is tied to the lifespan +of the native method call. The result is that, by default, handles +remain valid and the objects associated with these handles will be +held live for the lifespan of the native method call. + +In many cases, however, it is necessary that the handles remain valid for +either a shorter or longer lifespan than that of the native method. +The sections which follow describe the node-addon-api classes and +methods that than can be used to change the handle lifespan from +the default. + +## Making handle lifespan shorter than that of the native method + +It is often necessary to make the lifespan of handles shorter than +the lifespan of a native method. For example, consider a native method +that has a loop which creates a number of values and does something +with each of the values, one at a time: + +```C++ +for (int i = 0; i < LOOP_MAX; i++) { + std::string name = std::string("inner-scope") + std::to_string(i); + Value newValue = String::New(info.Env(), name.c_str()); + // do something with neValue +}; +``` + +This would result in a large number of handles being created, consuming +substantial resources. In addition, even though the native code could only +use the most recently created value, all of the previously created +values would also be kept alive since they all share the same scope. + +To handle this case, node-addon-api provides the ability to establish +a new 'scope' to which newly created handles will be associated. Once those +handles are no longer required, the scope can be deleted and any handles +associated with the scope are invalidated. The `HandleScope` +and `EscapableHandleScope` classes are provided by node-addon-api for +creating additional scopes. + +node-addon-api only supports a single nested hierarchy of scopes. There is +only one active scope at any time, and all new handles will be associated +with that scope while it is active. Scopes must be deleted in the reverse +order from which they are opened. In addition, all scopes created within +a native method must be deleted before returning from that method. Since +HandleScopes are typically stack allocated the compiler will take care of +deletion, however, care must be taken to create the scope in the right +place such that you achieve the desired lifetime. + +Taking the earlier example, creating a HandleScope in the innner loop +would ensure that at most a single new value is held alive throughout the +execution of the loop: + +```C +for (int i = 0; i < LOOP_MAX; i++) { + HandleScope scope(info.Env()); + std::string name = std::string("inner-scope") + std::to_string(i); + Value newValue = String::New(info.Env(), name.c_str()); + // do something with neValue +}; +``` + +When nesting scopes, there are cases where a handle from an +inner scope needs to live beyond the lifespan of that scope. node-addon-api +provides the `EscapableHandleScope` with the Escape method +in order to support this case. An escapable scope +allows one object to be 'promoted' so that it 'escapes' the +current scope and the lifespan of the handle changes from the current +scope to that of the outer scope. The Escape method can only be called +once for a given EscapableHandleScope. + diff --git a/doc/onject_lifetime_management.md b/doc/onject_lifetime_management.md deleted file mode 100644 index cf9d2a200..000000000 --- a/doc/onject_lifetime_management.md +++ /dev/null @@ -1,5 +0,0 @@ -# Object lifetime management - -You are reading a draft of the next documentation and it's in continuos update so -if you don't find what you need please refer to: -[C++ wrapper classes for the ABI-stable C APIs for Node.js](https://nodejs.github.io/node-addon-api/) \ No newline at end of file diff --git a/test/binding.cc b/test/binding.cc index 6543f6bbf..8e6a1e9ec 100644 --- a/test/binding.cc +++ b/test/binding.cc @@ -12,6 +12,7 @@ Object InitDataViewReadWrite(Env env); Object InitError(Env env); Object InitExternal(Env env); Object InitFunction(Env env); +Object InitHandleScope(Env env); Object InitName(Env env); Object InitObject(Env env); Object InitPromise(Env env); @@ -31,6 +32,7 @@ Object Init(Env env, Object exports) { exports.Set("external", InitExternal(env)); exports.Set("function", InitFunction(env)); exports.Set("name", InitName(env)); + exports.Set("handlescope", InitHandleScope(env)); exports.Set("object", InitObject(env)); exports.Set("promise", InitPromise(env)); exports.Set("typedarray", InitTypedArray(env)); diff --git a/test/binding.gyp b/test/binding.gyp index 0344d133c..4e0050480 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -12,6 +12,7 @@ 'error.cc', 'external.cc', 'function.cc', + 'handlescope.cc', 'name.cc', 'object/delete_property.cc', 'object/get_property.cc', diff --git a/test/handlescope.cc b/test/handlescope.cc new file mode 100644 index 000000000..a976d3a8f --- /dev/null +++ b/test/handlescope.cc @@ -0,0 +1,56 @@ +#include "napi.h" +#include "string.h" + +using namespace Napi; + +Value createScope(const CallbackInfo& info) { + { + HandleScope scope(info.Env()); + String::New(info.Env(), "inner-scope"); + } + return String::New(info.Env(), "scope"); +} + +Value escapeFromScope(const CallbackInfo& info) { + Value result; + { + EscapableHandleScope scope(info.Env()); + result = scope.Escape(String::New(info.Env(), "inner-scope")); + } + return result; +} + +#define LOOP_MAX 1000000 +Value stressEscapeFromScope(const CallbackInfo& info) { + Value result; + for (int i = 0; i < LOOP_MAX; i++) { + EscapableHandleScope scope(info.Env()); + std::string name = std::string("inner-scope") + std::to_string(i); + Value newValue = String::New(info.Env(), name.c_str()); + if (i == (LOOP_MAX -1)) { + result = scope.Escape(newValue); + } + } + return result; +} + +Value doubleEscapeFromScope(const CallbackInfo& info) { + Value result; + { + EscapableHandleScope scope(info.Env()); + result = scope.Escape(String::New(info.Env(), "inner-scope")); + result = scope.Escape(String::New(info.Env(), "inner-scope")); + } + return result; +} + +Object InitHandleScope(Env env) { + Object exports = Object::New(env); + + exports["createScope"] = Function::New(env, createScope); + exports["escapeFromScope"] = Function::New(env, escapeFromScope); + exports["stressEscapeFromScope"] = Function::New(env, stressEscapeFromScope); + exports["doubleEscapeFromScope"] = Function::New(env, doubleEscapeFromScope); + + return exports; +} diff --git a/test/handlescope.js b/test/handlescope.js new file mode 100644 index 000000000..71cb89783 --- /dev/null +++ b/test/handlescope.js @@ -0,0 +1,15 @@ +'use strict'; +const buildType = process.config.target_defaults.default_configuration; +const assert = require('assert'); + +test(require(`./build/${buildType}/binding.node`)); +test(require(`./build/${buildType}/binding_noexcept.node`)); + +function test(binding) { + assert.strictEqual(binding.handlescope.createScope(), 'scope'); + assert.strictEqual(binding.handlescope.escapeFromScope(), 'inner-scope'); + assert.strictEqual(binding.handlescope.stressEscapeFromScope(), 'inner-scope999999'); + assert.throws(() => binding.handlescope.doubleEscapeFromScope(), + Error, + ' napi_escape_handle already called on scope'); +} diff --git a/test/index.js b/test/index.js index b12b3f212..5211bb467 100644 --- a/test/index.js +++ b/test/index.js @@ -18,6 +18,7 @@ let testModules = [ 'error', 'external', 'function', + 'handlescope', 'name', 'object/delete_property', 'object/get_property',