Skip to content

Commit 65e8f05

Browse files
author
Gabriel Schulhof
committed
n-api: add API for asynchronous functions
Bundle a uv_async_t and a napi_ref to make it possible to call into JS from another thread. The API accepts a void data and context pointer, an optional native-to-JS function argument marshaller, and a JS-to-native return value marshaller. Fixes: #13512
1 parent ae2bed9 commit 65e8f05

File tree

11 files changed

+721
-3
lines changed

11 files changed

+721
-3
lines changed

doc/api/n-api.md

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,7 @@ The documentation for N-API is structured as follows:
4747
* [Custom Asynchronous Operations][]
4848
* [Promises][]
4949
* [Script Execution][]
50+
* [Asynchronous Thread-safe Function Calls][]
5051

5152
The N-API is a C API that ensures ABI stability across Node.js versions
5253
and different compiler levels. However, we also understand that a C++
@@ -203,6 +204,36 @@ typedef void (*napi_async_complete_callback)(napi_env env,
203204
void* data);
204205
```
205206

207+
#### napi_threadsafe_function_marshal
208+
Function pointer used with asynchronous thread-safe function calls. The callback
209+
will be called on the main thread. Its purpose is to compute the JavaScript
210+
function context and its arguments from the native data associated with the
211+
thread-safe function and store them in `recv` and `argv`, respectively.
212+
Callback functions must satisfy the following signature:
213+
214+
```C
215+
typedef napi_status(*napi_threadsafe_function_marshal)(napi_env env,
216+
void* data,
217+
napi_value* recv,
218+
size_t argc,
219+
napi_value* argv);
220+
```
221+
222+
#### napi_threadsafe_function_process_result
223+
Function pointer used with asynchronous thread-safe function calls. The callback
224+
will be called on the main thread after the JavaScript function has returned.
225+
If an exception was thrown during the execution of the JavaScript function, it
226+
will be made available in the `error` parameter. The `result` parameter will
227+
have the function's return value. Both parameters may be `NULL`, but one of them
228+
will always be set.
229+
230+
```C
231+
typedef void(*napi_threadsafe_function_process_result)(napi_env env,
232+
void* data,
233+
napi_value error,
234+
napi_value result);
235+
```
236+
206237
## Error Handling
207238
N-API uses both return values and JavaScript exceptions for error handling.
208239
The following sections explain the approach for each case.
@@ -3705,6 +3736,105 @@ NAPI_EXTERN napi_status napi_get_uv_event_loop(napi_env env,
37053736
- `[in] env`: The environment that the API is invoked under.
37063737
- `[out] loop`: The current libuv loop instance.
37073738

3739+
<!-- it's very convenient to have all the anchors indexed -->
3740+
<!--lint disable no-unused-definitions remark-lint-->
3741+
## Asynchronous Thread-safe Function Calls
3742+
JavaScript functions can normally only be called from a native addon's main
3743+
thread. If an addon creates additional threads then N-API functions that require
3744+
a `napi_env`, `napi_value`, or `napi_ref` must not be called from those threads.
3745+
3746+
This API provides the type `napi_threadsafe_function` as well as APIs to create,
3747+
destroy, and call objects of this type. `napi_threadsafe_function` creates a
3748+
permanent reference to a `napi_value` that holds a JavaScript function, and
3749+
uses `uv_async_t` from libuv to coordinate calls to the JavaScript function from
3750+
all threads.
3751+
3752+
The user provides callbacks `marshal_cb` and `process_result_cb` to handle the
3753+
conversion of the native data to JavaScript function argfuments, and to process
3754+
the JavaScript function return value or a possible error condition,
3755+
respectively.
3756+
3757+
`napi_threadsafe_function` objects are destroyed by passing them to
3758+
`napi_delete_threadsafe_function()`. Make sure that all threads that have
3759+
references to the `napi_threadsafe_function` object are stopped before deleting
3760+
the object.
3761+
3762+
Since `uv_async_t` is used in the implementation, the caveat whereby multiple
3763+
invocations on the secondary thread may result in only one invocation of the
3764+
JavaScript function also applies to `napi_threadsafe_function`.
3765+
3766+
### napi_create_threadsafe_function
3767+
<!-- YAML
3768+
added: REPLACEME
3769+
-->
3770+
```C
3771+
NAPI_EXTERN napi_status
3772+
napi_create_threadsafe_function(napi_env env,
3773+
napi_value func,
3774+
void* data,
3775+
size_t argc,
3776+
napi_threadsafe_function_marshal marshal_cb,
3777+
napi_threadsafe_function_process_result
3778+
process_result_cb,
3779+
napi_threadsafe_function* result);
3780+
```
3781+
3782+
- `[in] env`: The environment that the API is invoked under.
3783+
- `[in] func`: The JavaScript function to call from another thread.
3784+
- `[in] data`: Optional data to attach to the resulting `napi_threadsafe_function`.
3785+
- `[in] context`: Optional context associated with `data`.
3786+
- `[in] argc`: Number of arguments the JavaScript function will have.
3787+
- `[in] marshal_cb`: Optional callback to convert `data` and `context` to
3788+
JavaScript function arguments. The callback will always be called on the main
3789+
thread.
3790+
- `[in] process_result_cb`: Optional callback to handle the return value and/or
3791+
exception resulting from the invocation of the JavaScript function. The callback
3792+
will always be called on the main thread.
3793+
- `[out] result`: The asynchronous thread-safe JavaScript function.
3794+
3795+
### napi_call_threadsafe_function
3796+
<!-- YAML
3797+
added: REPLACEME
3798+
-->
3799+
```C
3800+
NAPI_EXTERN napi_status
3801+
napi_call_threadsafe_function(napi_threadsafe_function func);
3802+
```
3803+
3804+
- `[in] func`: The asynchronous thread-safe JavaScript function to invoke. This
3805+
API may be called from any thread.
3806+
3807+
### napi_get_threadsafe_function_data
3808+
<!-- YAML
3809+
added: REPLACEME
3810+
-->
3811+
```C
3812+
NAPI_EXTERN napi_status
3813+
napi_get_threadsafe_function_data(napi_threadsafe_function func,
3814+
void** data);
3815+
```
3816+
3817+
- `[in] func`: The asynchronous thread-safe JavaScript function whose associated
3818+
data to retrieve.
3819+
- `[out] data`: Optional pointer to receive the data associated with the
3820+
thread-safe JavaScript function.
3821+
- `[out]: context`: Optional pointer to receive the context associated with the
3822+
thread-safe JavaScript function.
3823+
3824+
### napi_delete_threadsafe_function
3825+
<!-- YAML
3826+
added: REPLACEME
3827+
-->
3828+
```C
3829+
NAPI_EXTERN napi_status
3830+
napi_delete_threadsafe_function(napi_env env,
3831+
napi_threadsafe_function func);
3832+
```
3833+
3834+
- `[in] env`: The environment that the API is invoked under.
3835+
- `[in] func`: The asynchronous thread-safe JavaScript function to delete.
3836+
3837+
[Asynchronous Thread-safe Function Calls]: #n_api_asynchronous_thread-safe_function_calls
37083838
[Promises]: #n_api_promises
37093839
[Simple Asynchronous Operations]: #n_api_simple_asynchronous_operations
37103840
[Custom Asynchronous Operations]: #n_api_custom_asynchronous_operations

src/node_api.cc

Lines changed: 187 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@
1818
#include "node_api.h"
1919
#include "node_internals.h"
2020

21-
#define NAPI_VERSION 2
21+
#define NAPI_VERSION 3
2222

2323
static
2424
napi_status napi_set_last_error(napi_env env, napi_status error_code,
@@ -3514,3 +3514,189 @@ napi_status napi_run_script(napi_env env,
35143514
*result = v8impl::JsValueFromV8LocalValue(script_result.ToLocalChecked());
35153515
return GET_RETURN_STATUS(env);
35163516
}
3517+
3518+
struct napi_threadsafe_function__ {
3519+
uv_async_t async;
3520+
napi_ref ref;
3521+
napi_env env;
3522+
size_t argc;
3523+
void* data;
3524+
void* context;
3525+
napi_threadsafe_function_marshal marshal_cb;
3526+
napi_threadsafe_function_process_result process_result_cb;
3527+
};
3528+
3529+
static napi_value napi_threadsafe_function_error(napi_env env,
3530+
const char* message) {
3531+
napi_value result, js_message;
3532+
if (napi_create_string_utf8(env, message, NAPI_AUTO_LENGTH, &js_message) ==
3533+
napi_ok) {
3534+
if (napi_create_error(env, nullptr, js_message, &result) == napi_ok) {
3535+
return result;
3536+
}
3537+
}
3538+
3539+
napi_fatal_error("N-API thread-safe function", NAPI_AUTO_LENGTH,
3540+
(std::string("Failed to create JS error: ") +
3541+
std::string(message)).c_str(), NAPI_AUTO_LENGTH);
3542+
return nullptr;
3543+
}
3544+
3545+
static void napi_threadsafe_function_cb(uv_async_t* uv_async) {
3546+
napi_threadsafe_function async =
3547+
node::ContainerOf(&napi_threadsafe_function__::async, uv_async);
3548+
v8::HandleScope handle_scope(async->env->isolate);
3549+
3550+
napi_value js_cb;
3551+
napi_value recv;
3552+
napi_value js_result = nullptr;
3553+
napi_value exception = nullptr;
3554+
std::vector<napi_value> argv(async->argc);
3555+
3556+
napi_status status = napi_get_reference_value(async->env, async->ref, &js_cb);
3557+
if (status != napi_ok) {
3558+
exception = napi_threadsafe_function_error(async->env,
3559+
"Failed to retrieve JS callback");
3560+
goto done;
3561+
}
3562+
3563+
status = async->marshal_cb(async->env, async->data, &recv, async->argc,
3564+
argv.data());
3565+
if (status != napi_ok) {
3566+
exception = napi_threadsafe_function_error(async->env,
3567+
"Failed to marshal JS callback arguments");
3568+
goto done;
3569+
}
3570+
3571+
status = napi_make_callback(async->env, nullptr, recv, js_cb, async->argc,
3572+
argv.data(), &js_result);
3573+
if (status != napi_ok) {
3574+
if (status == napi_pending_exception) {
3575+
status = napi_get_and_clear_last_exception(async->env, &exception);
3576+
if (status != napi_ok) {
3577+
exception = napi_threadsafe_function_error(async->env,
3578+
"Failed to retrieve JS callback exception");
3579+
goto done;
3580+
}
3581+
} else {
3582+
exception = napi_threadsafe_function_error(async->env,
3583+
"Failed to call JS callback");
3584+
goto done;
3585+
}
3586+
}
3587+
3588+
done:
3589+
async->process_result_cb(async->env, async->data, exception, js_result);
3590+
}
3591+
3592+
static napi_status napi_threadsafe_function_default_marshal(napi_env env,
3593+
void* data,
3594+
napi_value* recv,
3595+
size_t argc,
3596+
napi_value* argv) {
3597+
napi_status status;
3598+
for (size_t index = 0; index < argc; index++) {
3599+
status = napi_get_undefined(env, &argv[index]);
3600+
if (status != napi_ok) {
3601+
return status;
3602+
}
3603+
}
3604+
return napi_get_global(env, recv);
3605+
}
3606+
3607+
static void napi_threadsafe_function_default_process_result(napi_env env,
3608+
void* data,
3609+
napi_value error,
3610+
napi_value result) {
3611+
if (error != nullptr) {
3612+
napi_throw(env, error);
3613+
}
3614+
}
3615+
3616+
NAPI_EXTERN napi_status
3617+
napi_create_threadsafe_function(napi_env env,
3618+
napi_value func,
3619+
void* data,
3620+
size_t argc,
3621+
napi_threadsafe_function_marshal marshal_cb,
3622+
napi_threadsafe_function_process_result
3623+
process_result_cb,
3624+
napi_threadsafe_function* result) {
3625+
CHECK_ENV(env);
3626+
CHECK_ARG(env, func);
3627+
CHECK_ARG(env, result);
3628+
3629+
napi_valuetype func_type;
3630+
napi_status status = napi_typeof(env, func, &func_type);
3631+
if (status != napi_ok) {
3632+
return status;
3633+
}
3634+
3635+
if (func_type != napi_function) {
3636+
return napi_set_last_error(env, napi_function_expected);
3637+
}
3638+
3639+
napi_threadsafe_function async = new napi_threadsafe_function__;
3640+
if (async == nullptr) {
3641+
return napi_set_last_error(env, napi_generic_failure);
3642+
}
3643+
3644+
status = napi_create_reference(env, func, 1, &async->ref);
3645+
if (status != napi_ok) {
3646+
delete async;
3647+
return status;
3648+
}
3649+
3650+
if (uv_async_init(uv_default_loop(), &async->async,
3651+
napi_threadsafe_function_cb) != 0) {
3652+
napi_delete_reference(env, async->ref);
3653+
delete async;
3654+
return napi_set_last_error(env, napi_generic_failure);
3655+
}
3656+
3657+
async->argc = argc;
3658+
async->marshal_cb = marshal_cb == nullptr ?
3659+
napi_threadsafe_function_default_marshal : marshal_cb;
3660+
async->process_result_cb =
3661+
process_result_cb == nullptr ?
3662+
napi_threadsafe_function_default_process_result : process_result_cb;
3663+
async->data = data;
3664+
async->env = env;
3665+
3666+
*result = async;
3667+
return napi_clear_last_error(env);
3668+
}
3669+
3670+
NAPI_EXTERN napi_status
3671+
napi_get_threadsafe_function_data(napi_threadsafe_function async,
3672+
void** data) {
3673+
if (data != nullptr) {
3674+
*data = async->data;
3675+
}
3676+
return napi_ok;
3677+
}
3678+
3679+
NAPI_EXTERN napi_status
3680+
napi_call_threadsafe_function(napi_threadsafe_function async) {
3681+
return uv_async_send(&async->async) == 0 ?
3682+
napi_ok : napi_generic_failure;
3683+
}
3684+
3685+
NAPI_EXTERN napi_status
3686+
napi_delete_threadsafe_function(napi_env env,
3687+
napi_threadsafe_function async) {
3688+
CHECK_ENV(env);
3689+
CHECK_ARG(env, async);
3690+
3691+
napi_status status = napi_delete_reference(env, async->ref);
3692+
if (status != napi_ok) {
3693+
return status;
3694+
}
3695+
3696+
uv_close(reinterpret_cast<uv_handle_t*>(&async->async),
3697+
[] (uv_handle_t* handle) -> void {
3698+
delete handle;
3699+
});
3700+
3701+
return napi_clear_last_error(env);
3702+
}

src/node_api.h

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -587,6 +587,28 @@ NAPI_EXTERN napi_status napi_run_script(napi_env env,
587587
NAPI_EXTERN napi_status napi_get_uv_event_loop(napi_env env,
588588
struct uv_loop_s** loop);
589589

590+
// Calling into JS from other threads
591+
NAPI_EXTERN napi_status
592+
napi_create_threadsafe_function(napi_env env,
593+
napi_value func,
594+
void* data,
595+
size_t argc,
596+
napi_threadsafe_function_marshal marshal_cb,
597+
napi_threadsafe_function_process_result
598+
process_result_cb,
599+
napi_threadsafe_function* result);
600+
601+
NAPI_EXTERN napi_status
602+
napi_call_threadsafe_function(napi_threadsafe_function func);
603+
604+
NAPI_EXTERN napi_status
605+
napi_get_threadsafe_function_data(napi_threadsafe_function func,
606+
void** data);
607+
608+
NAPI_EXTERN napi_status
609+
napi_delete_threadsafe_function(napi_env env,
610+
napi_threadsafe_function func);
611+
590612
EXTERN_C_END
591613

592614
#endif // SRC_NODE_API_H_

0 commit comments

Comments
 (0)