Browse Source

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 <michael_dawson@ca.ibm.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
canary-base
Jason Ginchereau 8 years ago
committed by Michael Dawson
parent
commit
0c258bdc40
  1. 166
      doc/api/n-api.md
  2. 61
      src/node_api.cc
  3. 24
      src/node_api.h
  4. 1
      src/node_api_types.h
  5. 12
      test/addons-napi/test_make_callback/binding.cc
  6. 39
      test/addons-napi/test_make_callback/test.js
  7. 2
      test/addons-napi/test_make_callback_recurse/binding.cc

166
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 Properties][]
* [Working with JavaScript Functions][] * [Working with JavaScript Functions][]
* [Object Wrap][] * [Object Wrap][]
* [Asynchronous Operations][] * [Simple Asynchronous Operations][]
* [Custom Asynchronous Operations][]
* [Promises][] * [Promises][]
* [Script Execution][] * [Script Execution][]
@ -264,7 +265,7 @@ It is intended only for logging purposes.
added: v8.0.0 added: v8.0.0
--> -->
```C ```C
NAPI_EXTERN napi_status napi_status
napi_get_last_error_info(napi_env env, napi_get_last_error_info(napi_env env,
const napi_extended_error_info** result); const napi_extended_error_info** result);
``` ```
@ -515,7 +516,7 @@ This API returns a JavaScript RangeError with the text provided.
added: v8.0.0 added: v8.0.0
--> -->
```C ```C
NAPI_EXTERN napi_status napi_get_and_clear_last_exception(napi_env env, napi_status napi_get_and_clear_last_exception(napi_env env,
napi_value* result); napi_value* result);
``` ```
@ -531,7 +532,7 @@ This API returns true if an exception is pending.
added: v8.0.0 added: v8.0.0
--> -->
```C ```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. - `[in] env`: The environment that the API is invoked under.
@ -551,7 +552,7 @@ thrown to immediately terminate the process.
added: v8.2.0 added: v8.2.0
--> -->
```C ```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. - `[in] location`: Optional location at which the error occurred.
@ -718,7 +719,7 @@ reverse order from which they were created.
added: v8.0.0 added: v8.0.0
--> -->
```C ```C
NAPI_EXTERN napi_status napi_escape_handle(napi_env env, napi_status napi_escape_handle(napi_env env,
napi_escapable_handle_scope scope, napi_escapable_handle_scope scope,
napi_value escapee, napi_value escapee,
napi_value* result); napi_value* result);
@ -1478,7 +1479,7 @@ of the ECMAScript Language Specification.
added: v8.0.0 added: v8.0.0
--> -->
```C ```C
NAPI_EXTERN napi_status napi_create_string_latin1(napi_env env, napi_status napi_create_string_latin1(napi_env env,
const char* str, const char* str,
size_t length, size_t length,
napi_value* result); napi_value* result);
@ -1811,7 +1812,7 @@ JavaScript Number
added: v8.0.0 added: v8.0.0
--> -->
```C ```C
NAPI_EXTERN napi_status napi_get_value_string_latin1(napi_env env, napi_status napi_get_value_string_latin1(napi_env env,
napi_value value, napi_value value,
char* buf, char* buf,
size_t bufsize, size_t bufsize,
@ -2790,8 +2791,8 @@ in as arguments to the function.
Returns `napi_ok` if the API succeeded. Returns `napi_ok` if the API succeeded.
This method allows a JavaScript function object to be called from a native 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 add-on. This is the primary mechanism of calling back *from* the add-on's
native code *into* JavaScript. For special cases like calling into JavaScript native code *into* JavaScript. For the special case of calling into JavaScript
after an async operation, see [`napi_make_callback`][]. after an async operation, see [`napi_make_callback`][].
A sample use case might look as follows. Consider the following JavaScript 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. Returns `napi_ok` if the API succeeded.
### *napi_make_callback*
<!-- YAML
added: v8.0.0
-->
```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 ## Object Wrap
N-API offers a way to "wrap" C++ classes and instances so that the class 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 associated with the wrapping, it will no longer be called when the JavaScript
object becomes garbage-collected. object becomes garbage-collected.
## Asynchronous Operations ## Simple Asynchronous Operations
Addon modules often need to leverage async helpers from libuv as part of their 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 implementation. This allows them to schedule work to be executed asynchronously
@ -3250,7 +3218,7 @@ Once created the async worker can be queued
for execution using the [`napi_queue_async_work`][] function: for execution using the [`napi_queue_async_work`][] function:
```C ```C
NAPI_EXTERN napi_status napi_queue_async_work(napi_env env, napi_status napi_queue_async_work(napi_env env,
napi_async_work work); napi_async_work work);
``` ```
@ -3271,7 +3239,6 @@ changes:
description: Added `async_resource` and `async_resource_name` parameters. description: Added `async_resource` and `async_resource_name` parameters.
--> -->
```C ```C
NAPI_EXTERN
napi_status napi_create_async_work(napi_env env, napi_status napi_create_async_work(napi_env env,
napi_value async_resource, napi_value async_resource,
napi_value async_resource_name, napi_value async_resource_name,
@ -3314,7 +3281,7 @@ for more information.
added: v8.0.0 added: v8.0.0
--> -->
```C ```C
NAPI_EXTERN napi_status napi_delete_async_work(napi_env env, napi_status napi_delete_async_work(napi_env env,
napi_async_work work); napi_async_work work);
``` ```
@ -3330,7 +3297,7 @@ This API frees a previously allocated work object.
added: v8.0.0 added: v8.0.0
--> -->
```C ```C
NAPI_EXTERN napi_status napi_queue_async_work(napi_env env, napi_status napi_queue_async_work(napi_env env,
napi_async_work work); napi_async_work work);
``` ```
@ -3347,7 +3314,7 @@ for execution.
added: v8.0.0 added: v8.0.0
--> -->
```C ```C
NAPI_EXTERN napi_status napi_cancel_async_work(napi_env env, napi_status napi_cancel_async_work(napi_env env,
napi_async_work work); napi_async_work work);
``` ```
@ -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` `napi_cancelled`. The work should not be deleted before the `complete`
callback invocation, even if it has been successfully cancelled. 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**
<!-- YAML
added: REPLACEME
-->
```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**
<!-- YAML
added: REPLACEME
-->
```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*
<!-- YAML
added: v8.0.0
changes:
- version: REPLACEME
description: Added `async_context` parameter.
-->
```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 ## Version Management
### napi_get_node_version ### napi_get_node_version
@ -3378,7 +3432,6 @@ typedef struct {
const char* release; const char* release;
} napi_node_version; } napi_node_version;
NAPI_EXTERN
napi_status napi_get_node_version(napi_env env, napi_status napi_get_node_version(napi_env env,
const napi_node_version** version); const napi_node_version** version);
``` ```
@ -3399,7 +3452,7 @@ The returned buffer is statically allocated and does not need to be freed.
added: v8.0.0 added: v8.0.0
--> -->
```C ```C
NAPI_EXTERN napi_status napi_get_version(napi_env env, napi_status napi_get_version(napi_env env,
uint32_t* result); uint32_t* result);
``` ```
@ -3508,7 +3561,7 @@ deferred = NULL;
added: v8.5.0 added: v8.5.0
--> -->
```C ```C
NAPI_EXTERN napi_status napi_create_promise(napi_env env, napi_status napi_create_promise(napi_env env,
napi_deferred* deferred, napi_deferred* deferred,
napi_value* promise); napi_value* promise);
``` ```
@ -3528,7 +3581,7 @@ This API creates a deferred object and a JavaScript promise.
added: v8.5.0 added: v8.5.0
--> -->
```C ```C
NAPI_EXTERN napi_status napi_resolve_deferred(napi_env env, napi_status napi_resolve_deferred(napi_env env,
napi_deferred deferred, napi_deferred deferred,
napi_value resolution); napi_value resolution);
``` ```
@ -3551,7 +3604,7 @@ The deferred object is freed upon successful completion.
added: v8.5.0 added: v8.5.0
--> -->
```C ```C
NAPI_EXTERN napi_status napi_reject_deferred(napi_env env, napi_status napi_reject_deferred(napi_env env,
napi_deferred deferred, napi_deferred deferred,
napi_value rejection); napi_value rejection);
``` ```
@ -3574,7 +3627,7 @@ The deferred object is freed upon successful completion.
added: v8.5.0 added: v8.5.0
--> -->
```C ```C
NAPI_EXTERN napi_status napi_is_promise(napi_env env, napi_status napi_is_promise(napi_env env,
napi_value promise, napi_value promise,
bool* is_promise); bool* is_promise);
``` ```
@ -3604,7 +3657,8 @@ NAPI_EXTERN napi_status napi_run_script(napi_env env,
- `[out] result`: The value resulting from having executed the script. - `[out] result`: The value resulting from having executed the script.
[Promises]: #n_api_promises [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 [Basic N-API Data Types]: #n_api_basic_n_api_data_types
[ECMAScript Language Specification]: https://tc39.github.io/ecma262/ [ECMAScript Language Specification]: https://tc39.github.io/ecma262/
[Error Handling]: #n_api_error_handling [Error Handling]: #n_api_error_handling

61
src/node_api.cc

@ -2765,7 +2765,52 @@ napi_status napi_instanceof(napi_env env,
return GET_RETURN_STATUS(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<v8::Context> context = isolate->GetCurrentContext();
v8::Local<v8::Object> 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::String> 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<napi_async_context>(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<node::async_context*>(async_context);
node::EmitAsyncDestroy(isolate, *node_async_context);
return napi_clear_last_error(env);
}
napi_status napi_make_callback(napi_env env, napi_status napi_make_callback(napi_env env,
napi_async_context async_context,
napi_value recv, napi_value recv,
napi_value func, napi_value func,
size_t argc, size_t argc,
@ -2786,12 +2831,22 @@ napi_status napi_make_callback(napi_env env,
v8::Local<v8::Function> v8func; v8::Local<v8::Function> v8func;
CHECK_TO_FUNCTION(env, v8func, func); CHECK_TO_FUNCTION(env, v8func, func);
v8::Local<v8::Value> callback_result = node::MakeCallback( node::async_context* node_async_context =
reinterpret_cast<node::async_context*>(async_context);
if (node_async_context == nullptr) {
static node::async_context empty_context = { 0, 0 };
node_async_context = &empty_context;
}
v8::MaybeLocal<v8::Value> callback_result = node::MakeCallback(
isolate, v8recv, v8func, argc, isolate, v8recv, v8func, argc,
reinterpret_cast<v8::Local<v8::Value>*>(const_cast<napi_value*>(argv))); reinterpret_cast<v8::Local<v8::Value>*>(const_cast<napi_value*>(argv)),
*node_async_context);
CHECK_MAYBE_EMPTY(env, callback_result, napi_generic_failure);
if (result != nullptr) { if (result != nullptr) {
*result = v8impl::JsValueFromV8LocalValue(callback_result); *result = v8impl::JsValueFromV8LocalValue(
callback_result.ToLocalChecked());
} }
return GET_RETURN_STATUS(env); return GET_RETURN_STATUS(env);

24
src/node_api.h

@ -318,14 +318,6 @@ NAPI_EXTERN napi_status napi_instanceof(napi_env env,
napi_value constructor, napi_value constructor,
bool* result); 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 // Methods to work with napi_callbacks
// Gets all callback info in a single call. (Ugly, but faster.) // 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_EXTERN napi_status napi_cancel_async_work(napi_env env,
napi_async_work work); 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 // version management
NAPI_EXTERN napi_status napi_get_version(napi_env env, uint32_t* result); NAPI_EXTERN napi_status napi_get_version(napi_env env, uint32_t* result);

1
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_handle_scope__ *napi_handle_scope;
typedef struct napi_escapable_handle_scope__ *napi_escapable_handle_scope; typedef struct napi_escapable_handle_scope__ *napi_escapable_handle_scope;
typedef struct napi_callback_info__ *napi_callback_info; 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_async_work__ *napi_async_work;
typedef struct napi_deferred__ *napi_deferred; typedef struct napi_deferred__ *napi_deferred;

12
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_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; napi_value result;
if (func_type == napi_function) { if (func_type == napi_function) {
NAPI_CALL(env, NAPI_CALL(env, napi_make_callback(
napi_make_callback(env, recv, func, argv.size(), argv.data(), &result)); env, context, recv, func, argv.size(), argv.data(), &result));
} else { } else {
NAPI_ASSERT(env, false, "Unexpected argument type"); NAPI_ASSERT(env, false, "Unexpected argument type");
} }
NAPI_CALL(env, napi_async_destroy(env, context));
return result; return result;
} }

39
test/addons-napi/test_make_callback/test.js

@ -2,12 +2,12 @@
const common = require('../../common'); const common = require('../../common');
const assert = require('assert'); const assert = require('assert');
const async_hooks = require('async_hooks');
const vm = require('vm'); const vm = require('vm');
const binding = require(`./build/${common.buildType}/binding`); const binding = require(`./build/${common.buildType}/binding`);
const makeCallback = binding.makeCallback; const makeCallback = binding.makeCallback;
function myMultiArgFunc(arg1, arg2, arg3) { function myMultiArgFunc(arg1, arg2, arg3) {
console.log(`MyFunc was called with ${arguments.length} arguments`);
assert.strictEqual(arg1, 1); assert.strictEqual(arg1, 1);
assert.strictEqual(arg2, 2); assert.strictEqual(arg2, 2);
assert.strictEqual(arg3, 3); assert.strictEqual(arg3, 3);
@ -81,3 +81,40 @@ function endpoint($Object) {
} }
assert.strictEqual(Object, makeCallback(process, forward, endpoint)); 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();
});

2
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 recv = args[0];
napi_value func = args[1]; napi_value func = args[1];
napi_make_callback(env, napi_make_callback(env, nullptr /* async_context */,
recv, func, 0 /* argc */, nullptr /* argv */, nullptr /* result */); recv, func, 0 /* argc */, nullptr /* argv */, nullptr /* result */);
return recv; return recv;

Loading…
Cancel
Save