From 0c258bdc4040fcc4ab590fc80dbcd2182b4c74ae 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. PR-URL: https://github.com/nodejs/node/pull/15189 Fixes: https://github.com/nodejs/node/issues/13254 Reviewed-By: Michael Dawson Reviewed-By: James M Snell --- doc/api/n-api.md | 214 +++++++++++------- src/node_api.cc | 61 ++++- src/node_api.h | 24 +- src/node_api_types.h | 1 + .../addons-napi/test_make_callback/binding.cc | 12 +- test/addons-napi/test_make_callback/test.js | 39 +++- .../test_make_callback_recurse/binding.cc | 4 +- 7 files changed, 259 insertions(+), 96 deletions(-) diff --git a/doc/api/n-api.md b/doc/api/n-api.md index 4d95ad1ab06f79..d039e92589d1df 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][] * [Script Execution][] @@ -264,7 +265,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); ``` @@ -515,8 +516,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. @@ -531,7 +532,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. @@ -551,7 +552,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. @@ -718,10 +719,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. @@ -1478,10 +1479,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. @@ -1811,11 +1812,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. @@ -2790,8 +2791,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 @@ -3003,39 +3004,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 @@ -3214,7 +3182,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 @@ -3250,8 +3218,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 @@ -3271,7 +3239,6 @@ changes: description: Added `async_resource` and `async_resource_name` parameters. --> ```C -NAPI_EXTERN napi_status napi_create_async_work(napi_env env, napi_value async_resource, napi_value async_resource_name, @@ -3314,8 +3281,8 @@ for more information. 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. @@ -3330,8 +3297,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. @@ -3347,8 +3314,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. @@ -3363,6 +3330,93 @@ 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, + napi_value 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`: Required 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 @@ -3378,7 +3432,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); ``` @@ -3399,8 +3452,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. @@ -3508,9 +3561,9 @@ deferred = NULL; added: v8.5.0 --> ```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. @@ -3528,9 +3581,9 @@ This API creates a deferred object and a JavaScript promise. added: v8.5.0 --> ```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. @@ -3551,9 +3604,9 @@ The deferred object is freed upon successful completion. added: v8.5.0 --> ```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. @@ -3574,9 +3627,9 @@ The deferred object is freed upon successful completion. added: v8.5.0 --> ```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. @@ -3604,7 +3657,8 @@ NAPI_EXTERN napi_status napi_run_script(napi_env env, - `[out] result`: The value resulting from having executed the script. [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 a8b449ece5b8cc..ef515996484d69 100644 --- a/src/node_api.cc +++ b/src/node_api.cc @@ -2765,7 +2765,52 @@ napi_status napi_instanceof(napi_env env, return GET_RETURN_STATUS(env); } +napi_status napi_async_init(napi_env env, + napi_value async_resource, + napi_value async_resource_name, + napi_async_context* result) { + CHECK_ENV(env); + CHECK_ARG(env, async_resource_name); + CHECK_ARG(env, result); + + v8::Isolate* isolate = env->isolate; + v8::Local context = isolate->GetCurrentContext(); + + v8::Local v8_resource; + if (async_resource != nullptr) { + CHECK_TO_OBJECT(env, context, v8_resource, async_resource); + } else { + v8_resource = v8::Object::New(isolate); + } + + v8::Local v8_resource_name; + CHECK_TO_STRING(env, context, v8_resource_name, async_resource_name); + + // TODO(jasongin): Consider avoiding allocation here by using + // a tagged pointer with 2×31 bit fields instead. + node::async_context* async_context = new node::async_context(); + + *async_context = node::EmitAsyncInit(isolate, v8_resource, v8_resource_name); + *result = reinterpret_cast(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, @@ -2786,12 +2831,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 cdc3a6bdb7ebe3..c7d5e746f1b98d 100644 --- a/src/node_api.h +++ b/src/node_api.h @@ -318,14 +318,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.) @@ -535,6 +527,22 @@ NAPI_EXTERN napi_status napi_queue_async_work(napi_env env, 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, + napi_value 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_make_callback/binding.cc b/test/addons-napi/test_make_callback/binding.cc index 2f52112e747fb8..df2c4dc672ea47 100644 --- a/test/addons-napi/test_make_callback/binding.cc +++ b/test/addons-napi/test_make_callback/binding.cc @@ -24,14 +24,22 @@ napi_value MakeCallback(napi_env env, napi_callback_info info) { NAPI_CALL(env, napi_typeof(env, func, &func_type)); + napi_value resource_name; + NAPI_CALL(env, napi_create_string_utf8(env, "test", -1, &resource_name)); + + napi_async_context context; + NAPI_CALL(env, napi_async_init(env, func, resource_name, &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..0e94caf1d975f2 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. +const hook_result = { + id: null, + init_called: false, + before_called: false, + after_called: false, + destroy_called: false, +}; +const 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 ce75adc5b87b11..1e0c16c80ee664 100644 --- a/test/addons-napi/test_make_callback_recurse/binding.cc +++ b/test/addons-napi/test_make_callback_recurse/binding.cc @@ -12,8 +12,8 @@ napi_value MakeCallback(napi_env env, napi_callback_info info) { napi_value recv = args[0]; napi_value func = args[1]; - napi_make_callback(env, - recv, func, 0 /* argc */, nullptr /* argv */, nullptr /* result */); + napi_make_callback(env, nullptr /* async_context */, + recv, func, 0 /* argc */, nullptr /* argv */, nullptr /* result */); return recv; }