diff --git a/README.md b/README.md index b3a97d0ac..ee673a972 100644 --- a/README.md +++ b/README.md @@ -93,6 +93,7 @@ still a work in progress as its not yet complete). - [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) diff --git a/doc/async_context.md b/doc/async_context.md new file mode 100644 index 000000000..a40813b8d --- /dev/null +++ b/doc/async_context.md @@ -0,0 +1,149 @@ +# AsyncContext + +The [AsyncWorker](async_worker.md) class may not be appropriate for every scenario, because with +those the async execution still happens on the main event loop. When using any +other async mechanism, introducing a new class `AsyncContext` is necessary to +ensure an async operation is properly tracked by the runtime. +The `AsyncContext` class provides `MakeCallback()` method to properly restore +the correct async execution context. + +## Methods + +### MakeCallback + +This method is used to call from native code back into JavaScript after +returning from an async operation (when there is no other script on the stack). + +```cpp +Value MakeCallback() const +``` + +Returns a `Value` representing the JavaScript object returned. + +### MakeCallback + +This method is used to call from native code back into JavaScript after +returning from an async operation (when there is no other script on the stack). + +```cpp +Value MakeCallback(const Object& receiver) const +``` + +- `[in] receiver`: The `this` object passed to the callback. + +Returns a `Value` representing the JavaScript object returned. + +### MakeCallback + +This method is used to call from native code back into JavaScript after +returning from an async operation (when there is no other script on the stack). + +```cpp +Value MakeCallback(const std::initializer_list& args) const +``` + +- `[in] args`: Initializer list of JavaScript values as `napi_value` +representing the arguments to the callback. + +Returns a `Value` representing the JavaScript object returned. + +### MakeCallback + +This method is used to call from native code back into JavaScript after +returning from an async operation (when there is no other script on the stack). + +```cpp +Value MakeCallback(const Object& receiver, const std::initializer_list& args) const +``` + +- `[in] receiver`: The `this` object passed to the callback. +- `[in] args`: Initializer list of JavaScript values as `napi_value` +representing the arguments to the callback. + +Returns a `Value` representing the JavaScript object returned. + +### Constructor + +Creates a new `AsyncContext`. + +```cpp +explicit AsyncContext(const char* resource_name, const Function& callback); +``` + +- `[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] callback`: The function which will be called when an asynchronous +operations ends. + +Returns an AsyncContext instance which can later make the given callback by +`MakeCallback()` method. + +### Constructor + +Creates a new `AsyncContext`. + +```cpp +explicit AsyncContext(const char* resource_name, const Object& resource, const Function& callback); +``` + +- `[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. +- `[in] callback`: The function which will be called when an asynchronous +operations ends. + +Returns an AsyncContext instance which can later make the given callback by +`MakeCallback()` method. + +### Destructor + +The async context to be destroyed. + +```cpp +virtual ~AsyncContext(); +``` + +## Example + +```cpp +#include "napi.h" +#include "uv.h" + +using namespace Napi; + +void HeavyWork(uv_work_t* request) { + ... +} + +void DidHeavyWork(uv_work_t* request, int status) { + HandleScope scope; + AsyncContext* context = static_cast(request->data); + + // Run the callback in the async context. + context->MakeCallback(); + + // The async context is destroyed. + delete context; +} + +void RunAsync(const CallbackInfo& info) { + Function callback = info[0].As(); + + // Creat a new async context instance. + AsyncContext* new AsyncContext("TestResource", callback); + + // You can queue a task with the async context to the custom async mechanism. + // This example is using `libuv` as custom async mechanism but it can be + // anything. + uv_work_t* request = new uv_work_t(); + request->data = context; + uv_queue_work( + uv_default_loop(), + request, + HeavyWork, + reinterpret_cast(DidHeavyWork)); +} +``` diff --git a/doc/async_operations.md b/doc/async_operations.md index b8dec37cf..019feb0f6 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/napi-inl.h b/napi-inl.h index 46ba2c0b9..2a036f64c 100644 --- a/napi-inl.h +++ b/napi-inl.h @@ -3074,6 +3074,73 @@ inline Value EscapableHandleScope::Escape(napi_value escapee) { return Value(_env, result); } +//////////////////////////////////////////////////////////////////////////////// +// AsyncContext class +//////////////////////////////////////////////////////////////////////////////// + +inline AsyncContext::AsyncContext(const char* resource_name, + const Function& callback) + : AsyncContext(resource_name, Object::New(callback.Env()), callback) { +} + +inline AsyncContext::AsyncContext(const char* resource_name, + const Object& resource, + const Function& callback) + : _env(callback.Env()), + _context(nullptr), + _callback(Napi::Persistent(callback)) { + napi_value resource_id; + napi_status status = napi_create_string_utf8( + _env, resource_name, NAPI_AUTO_LENGTH, &resource_id); + NAPI_THROW_IF_FAILED(_env, status); + + status = napi_async_init(_env, resource, resource_id, &_context); + NAPI_THROW_IF_FAILED(_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; + _callback = std::move(other._callback); +} + +inline AsyncContext& AsyncContext::operator =(AsyncContext&& other) { + _env = other._env; + other._env = nullptr; + _context = other._context; + other._context = nullptr; + _callback = std::move(other._callback); + return *this; +} + +inline Value AsyncContext::MakeCallback() const { + return MakeCallback(Object::New(_env)); +} + +inline Value AsyncContext::MakeCallback(const Object& receiver) const { + return _callback.MakeCallback(receiver, std::initializer_list{}); +} + +inline Value AsyncContext::MakeCallback( + const std::initializer_list& args) const { + return MakeCallback(Object::New(_env), args); +} + +inline Value AsyncContext::MakeCallback( + const Object& receiver, + const std::initializer_list& args) const { + return _callback.MakeCallback(receiver, args); +} + //////////////////////////////////////////////////////////////////////////////// // AsyncWorker class //////////////////////////////////////////////////////////////////////////////// diff --git a/napi.h b/napi.h index f9852968b..d13a6d1a8 100644 --- a/napi.h +++ b/napi.h @@ -1495,6 +1495,31 @@ namespace Napi { napi_escapable_handle_scope _scope; }; + class AsyncContext { + public: + explicit AsyncContext(const char* resource_name, const Function& callback); + explicit AsyncContext(const char* resource_name, + const Object& resource, + const Function& callback); + virtual ~AsyncContext(); + + AsyncContext(AsyncContext&& other); + AsyncContext& operator =(AsyncContext&& other); + AsyncContext(const AsyncContext&) = delete; + AsyncContext& operator =(AsyncContext&) = delete; + + Value MakeCallback() const; + Value MakeCallback(const Object& receiver) const; + Value MakeCallback(const std::initializer_list& args) const; + Value MakeCallback(const Object& receiver, + const std::initializer_list& args) const; + + private: + napi_env _env; + napi_async_context _context; + FunctionReference _callback; + }; + class AsyncWorker { public: virtual ~AsyncWorker(); diff --git a/test/asynccontext.cc b/test/asynccontext.cc new file mode 100644 index 000000000..1375615b3 --- /dev/null +++ b/test/asynccontext.cc @@ -0,0 +1,26 @@ +#include "napi.h" + +using namespace Napi; + +namespace { + +static AsyncContext* context; + +static void CreateAsyncContext(const CallbackInfo& info) { + Function callback = info[0].As(); + context = new AsyncContext("TestResource", callback); +} + +static void MakeCallback(const CallbackInfo& info) { + context->MakeCallback(); + delete context; +} + +} // end anonymous namespace + +Object InitAsyncContext(Env env) { + Object exports = Object::New(env); + exports["createAsyncContext"] = Function::New(env, CreateAsyncContext); + exports["makeCallback"] = Function::New(env, MakeCallback); + return exports; +} diff --git a/test/asynccontext.js b/test/asynccontext.js new file mode 100644 index 000000000..fe5a7e4a9 --- /dev/null +++ b/test/asynccontext.js @@ -0,0 +1,16 @@ +'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) { + let called = false; + binding.asynccontext.createAsyncContext(function() { + called = true; + }); + assert.strictEqual(called, false); + binding.asynccontext.makeCallback(); + assert.strictEqual(called, true); +} diff --git a/test/binding.cc b/test/binding.cc index 0032c8e30..1a67a31e1 100644 --- a/test/binding.cc +++ b/test/binding.cc @@ -3,6 +3,7 @@ using namespace Napi; Object InitArrayBuffer(Env env); +Object InitAsyncContext(Env env); Object InitAsyncWorker(Env env); Object InitBasicTypesNumber(Env env); Object InitBasicTypesValue(Env env); @@ -22,6 +23,7 @@ Object InitObjectWrap(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_number", InitBasicTypesNumber(env)); exports.Set("basic_types_value", InitBasicTypesValue(env)); diff --git a/test/binding.gyp b/test/binding.gyp index acbbc0a12..291100ff8 100644 --- a/test/binding.gyp +++ b/test/binding.gyp @@ -2,6 +2,7 @@ 'target_defaults': { 'sources': [ 'arraybuffer.cc', + 'asynccontext.cc', 'asyncworker.cc', 'basic_types/number.cc', 'basic_types/value.cc', diff --git a/test/index.js b/test/index.js index c9d7fb0c2..23f31f034 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/number', 'basic_types/value',