From 53de2e13a7f688fa6b4dc1007fe0f339ba0060ab Mon Sep 17 00:00:00 2001 From: Jason Ginchereau Date: Sat, 26 Aug 2017 15:44:43 -0700 Subject: [PATCH] n-api: Context for custom async operations - Add napi_async_context opaque pointer type. (If needed, we could later add APIs for getting the async IDs out of this context.) - Add napi_async_init() and napi_async_destroy() APIs. - Add async_context parameter to napi_make_callback(). - Add code and checks to test_make_callback to validate async context APIs by checking async hooks are called with correct context. - Update API documentation. Fixes: https://github.com/nodejs/node/issues/13254 --- doc/api/n-api.md | 213 +++++++++++------- src/node_api.cc | 56 ++++- src/node_api.h | 25 +- src/node_api_types.h | 1 + test/addons-napi/test_async/test_async.cc | 4 +- .../addons-napi/test_make_callback/binding.cc | 9 +- test/addons-napi/test_make_callback/test.js | 39 +++- .../test_make_callback_recurse/binding.cc | 2 +- 8 files changed, 252 insertions(+), 97 deletions(-) diff --git a/doc/api/n-api.md b/doc/api/n-api.md index e41465475ea72a..a6984d9a16b9fb 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -41,7 +41,8 @@ The documentation for N-API is structured as follows: * [Working with JavaScript Properties][] * [Working with JavaScript Functions][] * [Object Wrap][] -* [Asynchronous Operations][] +* [Simple Asynchronous Operations][] +* [Custom Asynchronous Operations][] * [Promises][] The N-API is a C API that ensures ABI stability across Node.js versions @@ -263,7 +264,7 @@ It is intended only for logging purposes. added: v8.0.0 --> ```C -NAPI_EXTERN napi_status +napi_status napi_get_last_error_info(napi_env env, const napi_extended_error_info** result); ``` @@ -514,8 +515,8 @@ This API returns a JavaScript RangeError with the text provided. added: v8.0.0 --> ```C -NAPI_EXTERN napi_status napi_get_and_clear_last_exception(napi_env env, - napi_value* result); +napi_status napi_get_and_clear_last_exception(napi_env env, + napi_value* result); ``` - `[in] env`: The environment that the API is invoked under. @@ -530,7 +531,7 @@ This API returns true if an exception is pending. added: v8.0.0 --> ```C -NAPI_EXTERN napi_status napi_is_exception_pending(napi_env env, bool* result); +napi_status napi_is_exception_pending(napi_env env, bool* result); ``` - `[in] env`: The environment that the API is invoked under. @@ -550,7 +551,7 @@ thrown to immediately terminate the process. added: v8.2.0 --> ```C -NAPI_EXTERN NAPI_NO_RETURN void napi_fatal_error(const char* location, const char* message); +NAPI_NO_RETURN void napi_fatal_error(const char* location, const char* message); ``` - `[in] location`: Optional location at which the error occurred. @@ -717,10 +718,10 @@ reverse order from which they were created. added: v8.0.0 --> ```C -NAPI_EXTERN napi_status napi_escape_handle(napi_env env, - napi_escapable_handle_scope scope, - napi_value escapee, - napi_value* result); +napi_status napi_escape_handle(napi_env env, + napi_escapable_handle_scope scope, + napi_value escapee, + napi_value* result); ``` - `[in] env`: The environment that the API is invoked under. @@ -1468,10 +1469,10 @@ of the ECMAScript Language Specification. added: v8.0.0 --> ```C -NAPI_EXTERN napi_status napi_create_string_latin1(napi_env env, - const char* str, - size_t length, - napi_value* result); +napi_status napi_create_string_latin1(napi_env env, + const char* str, + size_t length, + napi_value* result); ``` - `[in] env`: The environment that the API is invoked under. @@ -1801,11 +1802,11 @@ JavaScript Number added: v8.0.0 --> ```C -NAPI_EXTERN napi_status napi_get_value_string_latin1(napi_env env, - napi_value value, - char* buf, - size_t bufsize, - size_t* result) +napi_status napi_get_value_string_latin1(napi_env env, + napi_value value, + char* buf, + size_t bufsize, + size_t* result) ``` - `[in] env`: The environment that the API is invoked under. @@ -2780,8 +2781,8 @@ in as arguments to the function. Returns `napi_ok` if the API succeeded. This method allows a JavaScript function object to be called from a native -add-on. This is an primary mechanism of calling back *from* the add-on's -native code *into* JavaScript. For special cases like calling into JavaScript +add-on. This is the primary mechanism of calling back *from* the add-on's +native code *into* JavaScript. For the special case of calling into JavaScript after an async operation, see [`napi_make_callback`][]. A sample use case might look as follows. Consider the following JavaScript @@ -2993,39 +2994,6 @@ status = napi_new_instance(env, constructor, argc, argv, &value); Returns `napi_ok` if the API succeeded. -### *napi_make_callback* - -```C -napi_status napi_make_callback(napi_env env, - napi_value recv, - napi_value func, - int argc, - const napi_value* argv, - napi_value* result) -``` - -- `[in] env`: The environment that the API is invoked under. -- `[in] recv`: The `this` object passed to the called function. -- `[in] func`: `napi_value` representing the JavaScript function -to be invoked. -- `[in] argc`: The count of elements in the `argv` array. -- `[in] argv`: Array of JavaScript values as `napi_value` -representing the arguments to the function. -- `[out] result`: `napi_value` representing the JavaScript object returned. - -Returns `napi_ok` if the API succeeded. - -This method allows a JavaScript function object to be called from a native -add-on. This API is similar to `napi_call_function`. However, it 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). It is a fairly simple -wrapper around `node::MakeCallback`. - -For an example on how to use `napi_make_callback`, see the section on -[Asynchronous Operations][]. - ## Object Wrap N-API offers a way to "wrap" C++ classes and instances so that the class @@ -3204,7 +3172,7 @@ restoring the JavaScript object's prototype chain. If a finalize callback was associated with the wrapping, it will no longer be called when the JavaScript object becomes garbage-collected. -## Asynchronous Operations +## Simple Asynchronous Operations Addon modules often need to leverage async helpers from libuv as part of their implementation. This allows them to schedule work to be executed asynchronously @@ -3240,8 +3208,8 @@ Once created the async worker can be queued for execution using the [`napi_queue_async_work`][] function: ```C -NAPI_EXTERN napi_status napi_queue_async_work(napi_env env, - napi_async_work work); +napi_status napi_queue_async_work(napi_env env, + napi_async_work work); ``` [`napi_cancel_async_work`][] can be used if the work needs @@ -3257,7 +3225,6 @@ callback invocation, even when it was cancelled. added: v8.0.0 --> ```C -NAPI_EXTERN napi_status napi_create_async_work(napi_env env, napi_async_execute_callback execute, napi_async_complete_callback complete, @@ -3286,8 +3253,8 @@ required. added: v8.0.0 --> ```C -NAPI_EXTERN napi_status napi_delete_async_work(napi_env env, - napi_async_work work); +napi_status napi_delete_async_work(napi_env env, + napi_async_work work); ``` - `[in] env`: The environment that the API is invoked under. @@ -3302,8 +3269,8 @@ This API frees a previously allocated work object. added: v8.0.0 --> ```C -NAPI_EXTERN napi_status napi_queue_async_work(napi_env env, - napi_async_work work); +napi_status napi_queue_async_work(napi_env env, + napi_async_work work); ``` - `[in] env`: The environment that the API is invoked under. @@ -3319,8 +3286,8 @@ for execution. added: v8.0.0 --> ```C -NAPI_EXTERN napi_status napi_cancel_async_work(napi_env env, - napi_async_work work); +napi_status napi_cancel_async_work(napi_env env, + napi_async_work work); ``` - `[in] env`: The environment that the API is invoked under. @@ -3335,6 +3302,92 @@ the `complete` callback will be invoked with a status value of `napi_cancelled`. The work should not be deleted before the `complete` callback invocation, even if it has been successfully cancelled. +## Custom Asynchronous Operations +The simple asynchronous work APIs above 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, the following APIs are +necessary to ensure an async operation is properly tracked by the runtime. + +### *napi_async_init** + +```C +napi_status napi_async_init(napi_env env, + napi_value async_resource, + const char* async_resource_name, + napi_async_context* result) +``` + +- `[in] env`: The environment that the API is invoked under. +- `[in] async_resource`: An optional object associated with the async work + that will be passed to possible async_hooks [`init` hooks][]. +- `[in] async_resource_name`: An identifier for the kind of resource that is + being provided for diagnostic information exposed by the `async_hooks` API. +- `[out] result`: The initialized async context. + +Returns `napi_ok` if the API succeeded. + +### *napi_async_destroy** + +```C +napi_status napi_async_destroy(napi_env env, + napi_async_context async_context); +``` + +- `[in] env`: The environment that the API is invoked under. +- `[in] async_context`: The async context to be destroyed. + +Returns `napi_ok` if the API succeeded. + +### *napi_make_callback* + +```C +napi_status napi_make_callback(napi_env env, + napi_async_context async_context, + napi_value recv, + napi_value func, + int argc, + const napi_value* argv, + napi_value* result) +``` + +- `[in] env`: The environment that the API is invoked under. +- `[in] async_context`: Context for the async operation that is + invoking the callback. This should normally be a value previously + obtained from [`napi_async_init`][]. However `NULL` is also allowed, + which indicates the current async context (if any) is to be used + for the callback. +- `[in] recv`: The `this` object passed to the called function. +- `[in] func`: `napi_value` representing the JavaScript function +to be invoked. +- `[in] argc`: The count of elements in the `argv` array. +- `[in] argv`: Array of JavaScript values as `napi_value` +representing the arguments to the function. +- `[out] result`: `napi_value` representing the JavaScript object returned. + +Returns `napi_ok` if the API succeeded. + +This method allows a JavaScript function object to be called from a native +add-on. This API is similar to `napi_call_function`. However, it 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). It is a fairly simple +wrapper around `node::MakeCallback`. + +Note it is NOT necessary to use `napi_make_callback` from within a +`napi_async_complete_callback`; in that situation the callback's async +context has already been set up, so a direct call to `napi_call_function` +is sufficient and appropriate. Use of the `napi_make_callback` function +may be required when implementing custom async behavior that does not use +`napi_create_async_work`. + ## Version Management ### napi_get_node_version @@ -3350,7 +3403,6 @@ typedef struct { const char* release; } napi_node_version; -NAPI_EXTERN napi_status napi_get_node_version(napi_env env, const napi_node_version** version); ``` @@ -3371,8 +3423,8 @@ The returned buffer is statically allocated and does not need to be freed. added: v8.0.0 --> ```C -NAPI_EXTERN napi_status napi_get_version(napi_env env, - uint32_t* result); +napi_status napi_get_version(napi_env env, + uint32_t* result); ``` - `[in] env`: The environment that the API is invoked under. @@ -3455,9 +3507,9 @@ deferred = NULL; added: REPLACEME --> ```C -NAPI_EXTERN napi_status napi_create_promise(napi_env env, - napi_deferred* deferred, - napi_value* promise); +napi_status napi_create_promise(napi_env env, + napi_deferred* deferred, + napi_value* promise); ``` - `[in] env`: The environment that the API is invoked under. @@ -3475,9 +3527,9 @@ This API creates a deferred object and a JavaScript promise. added: REPLACEME --> ```C -NAPI_EXTERN napi_status napi_resolve_deferred(napi_env env, - napi_deferred deferred, - napi_value resolution); +napi_status napi_resolve_deferred(napi_env env, + napi_deferred deferred, + napi_value resolution); ``` - `[in] env`: The environment that the API is invoked under. @@ -3498,9 +3550,9 @@ The deferred object is freed upon successful completion. added: REPLACEME --> ```C -NAPI_EXTERN napi_status napi_reject_deferred(napi_env env, - napi_deferred deferred, - napi_value rejection); +napi_status napi_reject_deferred(napi_env env, + napi_deferred deferred, + napi_value rejection); ``` - `[in] env`: The environment that the API is invoked under. @@ -3521,9 +3573,9 @@ The deferred object is freed upon successful completion. added: REPLACEME --> ```C -NAPI_EXTERN napi_status napi_is_promise(napi_env env, - napi_value promise, - bool* is_promise); +napi_status napi_is_promise(napi_env env, + napi_value promise, + bool* is_promise); ``` - `[in] env`: The environment that the API is invoked under. @@ -3532,7 +3584,8 @@ NAPI_EXTERN napi_status napi_is_promise(napi_env env, object - that is, a promise object created by the underlying engine. [Promises]: #n_api_promises -[Asynchronous Operations]: #n_api_asynchronous_operations +[Simple Asynchronous Operations]: #n_api_asynchronous_operations +[Custom Asynchronous Operations]: #n_api_custom_asynchronous_operations [Basic N-API Data Types]: #n_api_basic_n_api_data_types [ECMAScript Language Specification]: https://tc39.github.io/ecma262/ [Error Handling]: #n_api_error_handling diff --git a/src/node_api.cc b/src/node_api.cc index 7a2b5bc48e1237..942a47bcff4991 100644 --- a/src/node_api.cc +++ b/src/node_api.cc @@ -2755,7 +2755,47 @@ napi_status napi_instanceof(napi_env env, return GET_RETURN_STATUS(env); } +napi_status napi_async_init(napi_env env, + napi_value async_resource, + const char* async_resource_name, + napi_async_context* result) { + CHECK_ENV(env); + CHECK_ARG(env, async_resource); + CHECK_ARG(env, async_resource_name); + CHECK_ARG(env, result); + + v8::Isolate* isolate = env->isolate; + v8::Local context = isolate->GetCurrentContext(); + + v8::Local v8resource; + CHECK_TO_OBJECT(env, context, v8resource, async_resource); + + // TODO(jasongin): Consider avoiding allocation here by using + // a tagged pointer with 2×31 bit fields instead. + node::async_context* node_async_context = new node::async_context(); + + *node_async_context = + node::EmitAsyncInit(isolate, v8resource, async_resource_name); + *result = reinterpret_cast(node_async_context); + + return napi_clear_last_error(env); +} + +napi_status napi_async_destroy(napi_env env, + napi_async_context async_context) { + CHECK_ENV(env); + CHECK_ARG(env, async_context); + + v8::Isolate* isolate = env->isolate; + node::async_context* node_async_context = + reinterpret_cast(async_context); + node::EmitAsyncDestroy(isolate, *node_async_context); + + return napi_clear_last_error(env); +} + napi_status napi_make_callback(napi_env env, + napi_async_context async_context, napi_value recv, napi_value func, size_t argc, @@ -2776,12 +2816,22 @@ napi_status napi_make_callback(napi_env env, v8::Local v8func; CHECK_TO_FUNCTION(env, v8func, func); - v8::Local callback_result = node::MakeCallback( + node::async_context* node_async_context = + reinterpret_cast(async_context); + if (node_async_context == nullptr) { + static node::async_context empty_context = { 0, 0 }; + node_async_context = &empty_context; + } + + v8::MaybeLocal callback_result = node::MakeCallback( isolate, v8recv, v8func, argc, - reinterpret_cast*>(const_cast(argv))); + reinterpret_cast*>(const_cast(argv)), + *node_async_context); + CHECK_MAYBE_EMPTY(env, callback_result, napi_generic_failure); if (result != nullptr) { - *result = v8impl::JsValueFromV8LocalValue(callback_result); + *result = v8impl::JsValueFromV8LocalValue( + callback_result.ToLocalChecked()); } return GET_RETURN_STATUS(env); diff --git a/src/node_api.h b/src/node_api.h index 6a4b2941879ff0..3d4e22bcaa99cf 100644 --- a/src/node_api.h +++ b/src/node_api.h @@ -320,14 +320,6 @@ NAPI_EXTERN napi_status napi_instanceof(napi_env env, napi_value constructor, bool* result); -// Napi version of node::MakeCallback(...) -NAPI_EXTERN napi_status napi_make_callback(napi_env env, - napi_value recv, - napi_value func, - size_t argc, - const napi_value* argv, - napi_value* result); - // Methods to work with napi_callbacks // Gets all callback info in a single call. (Ugly, but faster.) @@ -536,6 +528,23 @@ NAPI_EXTERN napi_status napi_cancel_async_work(napi_env env, napi_async_work work); +// Methods for custom handling of async operations +NAPI_EXTERN napi_status napi_async_init(napi_env env, + napi_value async_resource, + const char* async_resource_name, + napi_async_context* result); + +NAPI_EXTERN napi_status napi_async_destroy(napi_env env, + napi_async_context async_context); + +NAPI_EXTERN napi_status napi_make_callback(napi_env env, + napi_async_context async_context, + napi_value recv, + napi_value func, + size_t argc, + const napi_value* argv, + napi_value* result); + // version management NAPI_EXTERN napi_status napi_get_version(napi_env env, uint32_t* result); diff --git a/src/node_api_types.h b/src/node_api_types.h index ac8482bf9dcf9d..574cb6ff984f16 100644 --- a/src/node_api_types.h +++ b/src/node_api_types.h @@ -16,6 +16,7 @@ typedef struct napi_ref__ *napi_ref; typedef struct napi_handle_scope__ *napi_handle_scope; typedef struct napi_escapable_handle_scope__ *napi_escapable_handle_scope; typedef struct napi_callback_info__ *napi_callback_info; +typedef struct napi_async_context__ *napi_async_context; typedef struct napi_async_work__ *napi_async_work; typedef struct napi_deferred__ *napi_deferred; diff --git a/test/addons-napi/test_async/test_async.cc b/test/addons-napi/test_async/test_async.cc index ca76fa2d33b132..3703b681eb1c7b 100644 --- a/test/addons-napi/test_async/test_async.cc +++ b/test/addons-napi/test_async/test_async.cc @@ -61,7 +61,7 @@ void Complete(napi_env env, napi_status status, void* data) { napi_value result; NAPI_CALL_RETURN_VOID(env, - napi_make_callback(env, global, callback, 2, argv, &result)); + napi_call_function(env, global, callback, 2, argv, &result)); NAPI_CALL_RETURN_VOID(env, napi_delete_reference(env, c->_callback)); NAPI_CALL_RETURN_VOID(env, napi_delete_async_work(env, c->_request)); @@ -116,7 +116,7 @@ void CancelComplete(napi_env env, napi_status status, void* data) { NAPI_CALL_RETURN_VOID(env, napi_get_global(env, &global)); napi_value result; NAPI_CALL_RETURN_VOID(env, - napi_make_callback(env, global, callback, 0, nullptr, &result)); + napi_call_function(env, global, callback, 0, nullptr, &result)); } NAPI_CALL_RETURN_VOID(env, napi_delete_async_work(env, c->_request)); diff --git a/test/addons-napi/test_make_callback/binding.cc b/test/addons-napi/test_make_callback/binding.cc index 987b024098598c..a028881beb4ae8 100644 --- a/test/addons-napi/test_make_callback/binding.cc +++ b/test/addons-napi/test_make_callback/binding.cc @@ -24,14 +24,19 @@ napi_value MakeCallback(napi_env env, napi_callback_info info) { NAPI_CALL(env, napi_typeof(env, func, &func_type)); + napi_async_context context; + NAPI_CALL(env, napi_async_init(env, func, "test", &context)); + napi_value result; if (func_type == napi_function) { - NAPI_CALL(env, - napi_make_callback(env, recv, func, argv.size(), argv.data(), &result)); + NAPI_CALL(env, napi_make_callback( + env, context, recv, func, argv.size(), argv.data(), &result)); } else { NAPI_ASSERT(env, false, "Unexpected argument type"); } + NAPI_CALL(env, napi_async_destroy(env, context)); + return result; } diff --git a/test/addons-napi/test_make_callback/test.js b/test/addons-napi/test_make_callback/test.js index c4f24872bdb78e..de64864b12db4e 100644 --- a/test/addons-napi/test_make_callback/test.js +++ b/test/addons-napi/test_make_callback/test.js @@ -2,12 +2,12 @@ const common = require('../../common'); const assert = require('assert'); +const async_hooks = require('async_hooks'); const vm = require('vm'); const binding = require(`./build/${common.buildType}/binding`); const makeCallback = binding.makeCallback; function myMultiArgFunc(arg1, arg2, arg3) { - console.log(`MyFunc was called with ${arguments.length} arguments`); assert.strictEqual(arg1, 1); assert.strictEqual(arg2, 2); assert.strictEqual(arg3, 3); @@ -81,3 +81,40 @@ function endpoint($Object) { } assert.strictEqual(Object, makeCallback(process, forward, endpoint)); + +// Check async hooks integration using async context. +var hook_result = { + id: null, + init_called: false, + before_called: false, + after_called: false, + destroy_called: false, +}; +var test_hook = async_hooks.createHook({ + init: (id, type) => { + if (type === 'test') { + hook_result.id = id; + hook_result.init_called = true; + } + }, + before: (id) => { + if (id === hook_result.id) hook_result.before_called = true; + }, + after: (id) => { + if (id === hook_result.id) hook_result.after_called = true; + }, + destroy: (id) => { + if (id === hook_result.id) hook_result.destroy_called = true; + }, +}); + +test_hook.enable(); +makeCallback(process, function () {}); + +assert.strictEqual(hook_result.init_called, true); +assert.strictEqual(hook_result.before_called, true); +assert.strictEqual(hook_result.after_called, true); +setImmediate(() => { + assert.strictEqual(hook_result.destroy_called, true); + test_hook.disable(); +}); diff --git a/test/addons-napi/test_make_callback_recurse/binding.cc b/test/addons-napi/test_make_callback_recurse/binding.cc index 3f5a4c28b43524..8f168ce10ec737 100644 --- a/test/addons-napi/test_make_callback_recurse/binding.cc +++ b/test/addons-napi/test_make_callback_recurse/binding.cc @@ -12,7 +12,7 @@ napi_value MakeCallback(napi_env env, napi_callback_info info) { napi_value recv = args[0]; napi_value func = args[1]; - napi_make_callback(env, + napi_make_callback(env, nullptr /* async_context */, recv, func, 0 /* argc */, nullptr /* argv */, nullptr /* result */); return recv;