diff --git a/README.md b/README.md index febfbe6..4f5be71 100644 --- a/README.md +++ b/README.md @@ -103,6 +103,7 @@ The following is the documentation for node-addon-api. - [Memory Management](doc/memory_management.md) - [Async Operations](doc/async_operations.md) - [AsyncWorker](doc/async_worker.md) + - [AsyncContext](doc/async_context.md) - [Promises](doc/promises.md) - [Version management](doc/version_management.md) diff --git a/doc/async_context.md b/doc/async_context.md new file mode 100644 index 0000000..48de9c4 --- /dev/null +++ b/doc/async_context.md @@ -0,0 +1,76 @@ +# AsyncContext + +The [Napi::AsyncWorker](async_worker.md) class may not be appropriate for every +scenario. When using any other async mechanism, introducing a new class +`Napi::AsyncContext` is necessary to ensure an async operation is properly +tracked by the runtime. The `Napi::AsyncContext` class can be passed to +[Napi::Function::MakeCallback()](function.md) method to properly restore the +correct async execution context. + +## Methods + +### Constructor + +Creates a new `Napi::AsyncContext`. + +```cpp +explicit Napi::AsyncContext::AsyncContext(napi_env env, const char* resource_name); +``` + +- `[in] env`: The environment in which to create the `Napi::AsyncContext`. +- `[in] resource_name`: Null-terminated strings that represents the +identifier for the kind of resource that is being provided for diagnostic +information exposed by the `async_hooks` API. + +### Constructor + +Creates a new `Napi::AsyncContext`. + +```cpp +explicit Napi::AsyncContext::AsyncContext(napi_env env, const char* resource_name, const Napi::Object& resource); +``` + +- `[in] env`: The environment in which to create the `Napi::AsyncContext`. +- `[in] resource_name`: Null-terminated strings that represents the +identifier for the kind of resource that is being provided for diagnostic +information exposed by the `async_hooks` API. +- `[in] resource`: Object associated with the asynchronous operation that +will be passed to possible `async_hooks`. + +### Destructor + +The `Napi::AsyncContext` to be destroyed. + +```cpp +virtual Napi::AsyncContext::~AsyncContext(); +``` + +## Operator + +```cpp +Napi::AsyncContext::operator napi_async_context() const; +``` + +Returns the N-API `napi_async_context` wrapped by the `Napi::AsyncContext` +object. This can be used to mix usage of the C N-API and node-addon-api. + +## Example + +```cpp +#include "napi.h" + +void MakeCallbackWithAsyncContext(const Napi::CallbackInfo& info) { + Napi::Function callback = info[0].As(); + Napi::Object resource = info[1].As(); + + // Creat a new async context instance. + Napi::AsyncContext context(info.Env(), "async_context_test", resource); + + // Invoke the callback with the async context instance. + callback.MakeCallback(Napi::Object::New(info.Env()), + std::initializer_list{}, context); + + // The async context instance is automatically destroyed here because it's + // block-scope like `Napi::HandleScope`. +} +``` diff --git a/doc/async_operations.md b/doc/async_operations.md index ee445dd..be4f401 100644 --- a/doc/async_operations.md +++ b/doc/async_operations.md @@ -21,3 +21,9 @@ asynchronous operations: These class helps manage asynchronous operations through an abstraction of the concept of moving data between the **event loop** and **worker threads**. + +Also, the above class may not be appropriate for every scenario. When using any +other asynchronous mechanism, the following API is necessary to ensure an +asynchronous operation is properly tracked by the runtime: + +- **[AsyncContext](async_context.md)** diff --git a/doc/function.md b/doc/function.md index 3e8351b..efc7ed4 100644 --- a/doc/function.md +++ b/doc/function.md @@ -233,12 +233,16 @@ Returns a `Napi::Value` representing the JavaScript value returned by the functi Calls a Javascript function from a native add-on after an asynchronous operation. ```cpp -Napi::Value Napi::Function::MakeCallback(napi_value recv, const std::initializer_list& args) const; +Napi::Value Napi::Function::MakeCallback(napi_value recv, const std::initializer_list& args, napi_async_context context = nullptr) const; ``` - `[in] recv`: The `this` object passed to the called function. - `[in] args`: Initializer list of JavaScript values as `napi_value` representing the arguments of the function. +- `[in] context`: Context for the async operation that is invoking the callback. +This should normally be a value previously obtained from [Napi::AsyncContext](async_context.md). +However `nullptr` is also allowed, which indicates the current async context +(if any) is to be used for the callback. Returns a `Napi::Value` representing the JavaScript value returned by the function. @@ -247,12 +251,16 @@ Returns a `Napi::Value` representing the JavaScript value returned by the functi Calls a Javascript function from a native add-on after an asynchronous operation. ```cpp -Napi::Value Napi::Function::MakeCallback(napi_value recv, const std::vector& args) const; +Napi::Value Napi::Function::MakeCallback(napi_value recv, const std::vector& args, napi_async_context context = nullptr) const; ``` - `[in] recv`: The `this` object passed to the called function. - `[in] args`: List of JavaScript values as `napi_value` representing the arguments of the function. +- `[in] context`: Context for the async operation that is invoking the callback. +This should normally be a value previously obtained from [Napi::AsyncContext](async_context.md). +However `nullptr` is also allowed, which indicates the current async context +(if any) is to be used for the callback. Returns a `Napi::Value` representing the JavaScript value returned by the function. @@ -261,13 +269,17 @@ Returns a `Napi::Value` representing the JavaScript value returned by the functi Calls a Javascript function from a native add-on after an asynchronous operation. ```cpp -Napi::Value Napi::Function::MakeCallback(napi_value recv, size_t argc, const napi_value* args) const; +Napi::Value Napi::Function::MakeCallback(napi_value recv, size_t argc, const napi_value* args, napi_async_context context = nullptr) const; ``` - `[in] recv`: The `this` object passed to the called function. - `[in] argc`: The number of the arguments passed to the function. - `[in] args`: Array of JavaScript values as `napi_value` representing the arguments of the function. +- `[in] context`: Context for the async operation that is invoking the callback. +This should normally be a value previously obtained from [Napi::AsyncContext](async_context.md). +However `nullptr` is also allowed, which indicates the current async context +(if any) is to be used for the callback. Returns a `Napi::Value` representing the JavaScript value returned by the function. diff --git a/doc/function_reference.md b/doc/function_reference.md index a18a9b8..a7988ac 100644 --- a/doc/function_reference.md +++ b/doc/function_reference.md @@ -171,12 +171,16 @@ Calls a referenced JavaScript function from a native add-on after an asynchronou operation. ```cpp -Napi::Value Napi::FunctionReference::MakeCallback(napi_value recv, const std::initializer_list& args) const; +Napi::Value Napi::FunctionReference::MakeCallback(napi_value recv, const std::initializer_list& args, napi_async_context = nullptr) const; ``` - `[in] recv`: The `this` object passed to the referenced function when it's called. - `[in] args`: Initializer list of JavaScript values as `napi_value` representing the arguments of the referenced function. +- `[in] context`: Context for the async operation that is invoking the callback. +This should normally be a value previously obtained from [Napi::AsyncContext](async_context.md). +However `nullptr` is also allowed, which indicates the current async context +(if any) is to be used for the callback. Returns a `Napi::Value` representing the JavaScript object returned by the referenced function. @@ -187,12 +191,16 @@ Calls a referenced JavaScript function from a native add-on after an asynchronou operation. ```cpp -Napi::Value Napi::FunctionReference::MakeCallback(napi_value recv, const std::vector& args) const; +Napi::Value Napi::FunctionReference::MakeCallback(napi_value recv, const std::vector& args, napi_async_context context = nullptr) const; ``` - `[in] recv`: The `this` object passed to the referenced function when it's called. - `[in] args`: Vector of JavaScript values as `napi_value` representing the arguments of the referenced function. +- `[in] context`: Context for the async operation that is invoking the callback. +This should normally be a value previously obtained from [Napi::AsyncContext](async_context.md). +However `nullptr` is also allowed, which indicates the current async context +(if any) is to be used for the callback. Returns a `Napi::Value` representing the JavaScript object returned by the referenced function. @@ -203,13 +211,17 @@ Calls a referenced JavaScript function from a native add-on after an asynchronou operation. ```cpp -Napi::Value Napi::FunctionReference::MakeCallback(napi_value recv, size_t argc, const napi_value* args) const; +Napi::Value Napi::FunctionReference::MakeCallback(napi_value recv, size_t argc, const napi_value* args, napi_async_context context = nullptr) const; ``` - `[in] recv`: The `this` object passed to the referenced function when it's called. - `[in] argc`: The number of arguments passed to the referenced function. - `[in] args`: Array of JavaScript values as `napi_value` representing the arguments of the referenced function. +- `[in] context`: Context for the async operation that is invoking the callback. +This should normally be a value previously obtained from [Napi::AsyncContext](async_context.md). +However `nullptr` is also allowed, which indicates the current async context +(if any) is to be used for the callback. Returns a `Napi::Value` representing the JavaScript object returned by the referenced function. diff --git a/napi-inl.h b/napi-inl.h index a4b1d42..aead8b9 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -1651,20 +1651,27 @@ inline Value Function::Call(napi_value recv, size_t argc, const napi_value* args } inline Value Function::MakeCallback( - napi_value recv, const std::initializer_list& args) const { - return MakeCallback(recv, args.size(), args.begin()); + napi_value recv, + const std::initializer_list& args, + napi_async_context context) const { + return MakeCallback(recv, args.size(), args.begin(), context); } inline Value Function::MakeCallback( - napi_value recv, const std::vector& args) const { - return MakeCallback(recv, args.size(), args.data()); + napi_value recv, + const std::vector& args, + napi_async_context context) const { + return MakeCallback(recv, args.size(), args.data(), context); } inline Value Function::MakeCallback( - napi_value recv, size_t argc, const napi_value* args) const { + napi_value recv, + size_t argc, + const napi_value* args, + napi_async_context context) const { napi_value result; napi_status status = napi_make_callback( - _env, nullptr, recv, _value, argc, args, &result); + _env, context, recv, _value, argc, args, &result); NAPI_THROW_IF_FAILED(_env, status, Value()); return Value(_env, result); } @@ -2416,9 +2423,11 @@ inline Napi::Value FunctionReference::Call( } inline Napi::Value FunctionReference::MakeCallback( - napi_value recv, const std::initializer_list& args) const { + napi_value recv, + const std::initializer_list& args, + napi_async_context context) const { EscapableHandleScope scope(_env); - Napi::Value result = Value().MakeCallback(recv, args); + Napi::Value result = Value().MakeCallback(recv, args, context); if (scope.Env().IsExceptionPending()) { return Value(); } @@ -2426,9 +2435,11 @@ inline Napi::Value FunctionReference::MakeCallback( } inline Napi::Value FunctionReference::MakeCallback( - napi_value recv, const std::vector& args) const { + napi_value recv, + const std::vector& args, + napi_async_context context) const { EscapableHandleScope scope(_env); - Napi::Value result = Value().MakeCallback(recv, args); + Napi::Value result = Value().MakeCallback(recv, args, context); if (scope.Env().IsExceptionPending()) { return Value(); } @@ -2436,9 +2447,12 @@ inline Napi::Value FunctionReference::MakeCallback( } inline Napi::Value FunctionReference::MakeCallback( - napi_value recv, size_t argc, const napi_value* args) const { + napi_value recv, + size_t argc, + const napi_value* args, + napi_async_context context) const { EscapableHandleScope scope(_env); - Napi::Value result = Value().MakeCallback(recv, argc, args); + Napi::Value result = Value().MakeCallback(recv, argc, args, context); if (scope.Env().IsExceptionPending()) { return Value(); } @@ -3274,6 +3288,54 @@ inline Value EscapableHandleScope::Escape(napi_value escapee) { return Value(_env, result); } +//////////////////////////////////////////////////////////////////////////////// +// AsyncContext class +//////////////////////////////////////////////////////////////////////////////// + +inline AsyncContext::AsyncContext(napi_env env, const char* resource_name) + : AsyncContext(env, resource_name, Object::New(env)) { +} + +inline AsyncContext::AsyncContext(napi_env env, + const char* resource_name, + const Object& resource) + : _env(env), + _context(nullptr) { + napi_value resource_id; + napi_status status = napi_create_string_utf8( + _env, resource_name, NAPI_AUTO_LENGTH, &resource_id); + NAPI_THROW_IF_FAILED_VOID(_env, status); + + status = napi_async_init(_env, resource, resource_id, &_context); + NAPI_THROW_IF_FAILED_VOID(_env, status); +} + +inline AsyncContext::~AsyncContext() { + if (_context != nullptr) { + napi_async_destroy(_env, _context); + _context = nullptr; + } +} + +inline AsyncContext::AsyncContext(AsyncContext&& other) { + _env = other._env; + other._env = nullptr; + _context = other._context; + other._context = nullptr; +} + +inline AsyncContext& AsyncContext::operator =(AsyncContext&& other) { + _env = other._env; + other._env = nullptr; + _context = other._context; + other._context = nullptr; + return *this; +} + +inline AsyncContext::operator napi_async_context() const { + return _context; +} + //////////////////////////////////////////////////////////////////////////////// // AsyncWorker class //////////////////////////////////////////////////////////////////////////////// diff --git a/napi.h b/napi.h index 2e37530..ed86272 100644 --- a/napi.h +++ b/napi.h @@ -925,9 +925,16 @@ namespace Napi { Value Call(napi_value recv, const std::vector& args) const; Value Call(napi_value recv, size_t argc, const napi_value* args) const; - Value MakeCallback(napi_value recv, const std::initializer_list& args) const; - Value MakeCallback(napi_value recv, const std::vector& args) const; - Value MakeCallback(napi_value recv, size_t argc, const napi_value* args) const; + Value MakeCallback(napi_value recv, + const std::initializer_list& args, + napi_async_context context = nullptr) const; + Value MakeCallback(napi_value recv, + const std::vector& args, + napi_async_context context = nullptr) const; + Value MakeCallback(napi_value recv, + size_t argc, + const napi_value* args, + napi_async_context context = nullptr) const; Object New(const std::initializer_list& args) const; Object New(const std::vector& args) const; @@ -1099,9 +1106,16 @@ namespace Napi { Napi::Value Call(napi_value recv, const std::vector& args) const; Napi::Value Call(napi_value recv, size_t argc, const napi_value* args) const; - Napi::Value MakeCallback(napi_value recv, const std::initializer_list& args) const; - Napi::Value MakeCallback(napi_value recv, const std::vector& args) const; - Napi::Value MakeCallback(napi_value recv, size_t argc, const napi_value* args) const; + Napi::Value MakeCallback(napi_value recv, + const std::initializer_list& args, + napi_async_context context = nullptr) const; + Napi::Value MakeCallback(napi_value recv, + const std::vector& args, + napi_async_context context = nullptr) const; + Napi::Value MakeCallback(napi_value recv, + size_t argc, + const napi_value* args, + napi_async_context context = nullptr) const; Object New(const std::initializer_list& args) const; Object New(const std::vector& args) const; @@ -1583,6 +1597,24 @@ namespace Napi { napi_escapable_handle_scope _scope; }; + class AsyncContext { + public: + explicit AsyncContext(napi_env env, const char* resource_name); + explicit AsyncContext(napi_env env, const char* resource_name, const Object& resource); + virtual ~AsyncContext(); + + AsyncContext(AsyncContext&& other); + AsyncContext& operator =(AsyncContext&& other); + AsyncContext(const AsyncContext&) = delete; + AsyncContext& operator =(AsyncContext&) = delete; + + operator napi_async_context() const; + + private: + napi_env _env; + napi_async_context _context; + }; + class AsyncWorker { public: virtual ~AsyncWorker(); diff --git a/test/asynccontext.cc b/test/asynccontext.cc new file mode 100644 index 0000000..bb1acbb --- /dev/null +++ b/test/asynccontext.cc @@ -0,0 +1,21 @@ +#include "napi.h" + +using namespace Napi; + +namespace { + +static void MakeCallback(const CallbackInfo& info) { + Function callback = info[0].As(); + Object resource = info[1].As(); + AsyncContext context(info.Env(), "async_context_test", resource); + callback.MakeCallback(Object::New(info.Env()), + std::initializer_list{}, context); +} + +} // end anonymous namespace + +Object InitAsyncContext(Env env) { + Object exports = Object::New(env); + exports["makeCallback"] = Function::New(env, MakeCallback); + return exports; +} diff --git a/test/asynccontext.js b/test/asynccontext.js new file mode 100644 index 0000000..e9b4aab --- /dev/null +++ b/test/asynccontext.js @@ -0,0 +1,73 @@ +'use strict'; +const buildType = process.config.target_defaults.default_configuration; +const assert = require('assert'); +const common = require('./common'); + +// we only check async hooks on 8.x an higher were +// they are closer to working properly +const nodeVersion = process.versions.node.split('.')[0] +let async_hooks = undefined; +function checkAsyncHooks() { + if (nodeVersion >= 8) { + if (async_hooks == undefined) { + async_hooks = require('async_hooks'); + } + return true; + } + return false; +} + +test(require(`./build/${buildType}/binding.node`)); +test(require(`./build/${buildType}/binding_noexcept.node`)); + +function installAsyncHooksForTest() { + return new Promise((resolve, reject) => { + let id; + const events = []; + const hook = async_hooks.createHook({ + init(asyncId, type, triggerAsyncId, resource) { + if (id === undefined && type === 'async_context_test') { + id = asyncId; + events.push({ eventName: 'init', type, triggerAsyncId, resource }); + } + }, + before(asyncId) { + if (asyncId === id) { + events.push({ eventName: 'before' }); + } + }, + after(asyncId) { + if (asyncId === id) { + events.push({ eventName: 'after' }); + } + }, + destroy(asyncId) { + if (asyncId === id) { + events.push({ eventName: 'destroy' }); + hook.disable(); + resolve(events); + } + } + }).enable(); + }); +} + +function test(binding) { + binding.asynccontext.makeCallback(common.mustCall(), { foo: 'foo' }); + if (!checkAsyncHooks()) + return; + + const hooks = installAsyncHooksForTest(); + const triggerAsyncId = async_hooks.executionAsyncId(); + hooks.then(actual => { + assert.deepStrictEqual(actual, [ + { eventName: 'init', + type: 'async_context_test', + triggerAsyncId: triggerAsyncId, + resource: { foo: 'foo' } }, + { eventName: 'before' }, + { eventName: 'after' }, + { eventName: 'destroy' } + ]); + }).catch(common.mustNotCall()); +} diff --git a/test/binding.cc b/test/binding.cc index c2bd101..ffa3ec7 100644 --- a/test/binding.cc +++ b/test/binding.cc @@ -4,6 +4,7 @@ using namespace Napi; Object InitArrayBuffer(Env env); +Object InitAsyncContext(Env env); Object InitAsyncWorker(Env env); Object InitBasicTypesBoolean(Env env); Object InitBasicTypesNumber(Env env); @@ -31,6 +32,7 @@ Object InitVersionManagement(Env env); Object Init(Env env, Object exports) { exports.Set("arraybuffer", InitArrayBuffer(env)); + exports.Set("asynccontext", InitAsyncContext(env)); exports.Set("asyncworker", InitAsyncWorker(env)); exports.Set("basic_types_boolean", InitBasicTypesBoolean(env)); exports.Set("basic_types_number", InitBasicTypesNumber(env)); diff --git a/test/binding.gyp b/test/binding.gyp index 7bff35e..2eaccdb 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -5,6 +5,7 @@ 'target_defaults': { 'sources': [ 'arraybuffer.cc', + 'asynccontext.cc', 'asyncworker.cc', 'basic_types/boolean.cc', 'basic_types/number.cc', diff --git a/test/index.js b/test/index.js index 25b9066..a0dadf8 100644 --- a/test/index.js +++ b/test/index.js @@ -9,6 +9,7 @@ process.config.target_defaults.default_configuration = // explicit declaration as follows. let testModules = [ 'arraybuffer', + 'asynccontext', 'asyncworker', 'basic_types/boolean', 'basic_types/number',