Skip to content

Commit

Permalink
n-api: add API for asynchronous functions
Browse files Browse the repository at this point in the history
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
  • Loading branch information
Gabriel Schulhof committed Dec 27, 2017
1 parent ae2bed9 commit 65e8f05
Show file tree
Hide file tree
Showing 11 changed files with 721 additions and 3 deletions.
130 changes: 130 additions & 0 deletions doc/api/n-api.md
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ The documentation for N-API is structured as follows:
* [Custom Asynchronous Operations][]
* [Promises][]
* [Script Execution][]
* [Asynchronous Thread-safe Function Calls][]

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

#### napi_threadsafe_function_marshal
Function pointer used with asynchronous thread-safe function calls. The callback
will be called on the main thread. Its purpose is to compute the JavaScript
function context and its arguments from the native data associated with the
thread-safe function and store them in `recv` and `argv`, respectively.
Callback functions must satisfy the following signature:

```C
typedef napi_status(*napi_threadsafe_function_marshal)(napi_env env,
void* data,
napi_value* recv,
size_t argc,
napi_value* argv);
```
#### napi_threadsafe_function_process_result
Function pointer used with asynchronous thread-safe function calls. The callback
will be called on the main thread after the JavaScript function has returned.
If an exception was thrown during the execution of the JavaScript function, it
will be made available in the `error` parameter. The `result` parameter will
have the function's return value. Both parameters may be `NULL`, but one of them
will always be set.
```C
typedef void(*napi_threadsafe_function_process_result)(napi_env env,
void* data,
napi_value error,
napi_value result);
```

## Error Handling
N-API uses both return values and JavaScript exceptions for error handling.
The following sections explain the approach for each case.
Expand Down Expand Up @@ -3705,6 +3736,105 @@ NAPI_EXTERN napi_status napi_get_uv_event_loop(napi_env env,
- `[in] env`: The environment that the API is invoked under.
- `[out] loop`: The current libuv loop instance.

<!-- it's very convenient to have all the anchors indexed -->
<!--lint disable no-unused-definitions remark-lint-->
## Asynchronous Thread-safe Function Calls
JavaScript functions can normally only be called from a native addon's main
thread. If an addon creates additional threads then N-API functions that require
a `napi_env`, `napi_value`, or `napi_ref` must not be called from those threads.

This API provides the type `napi_threadsafe_function` as well as APIs to create,
destroy, and call objects of this type. `napi_threadsafe_function` creates a
permanent reference to a `napi_value` that holds a JavaScript function, and
uses `uv_async_t` from libuv to coordinate calls to the JavaScript function from
all threads.

The user provides callbacks `marshal_cb` and `process_result_cb` to handle the
conversion of the native data to JavaScript function argfuments, and to process
the JavaScript function return value or a possible error condition,
respectively.

`napi_threadsafe_function` objects are destroyed by passing them to
`napi_delete_threadsafe_function()`. Make sure that all threads that have
references to the `napi_threadsafe_function` object are stopped before deleting
the object.

Since `uv_async_t` is used in the implementation, the caveat whereby multiple
invocations on the secondary thread may result in only one invocation of the
JavaScript function also applies to `napi_threadsafe_function`.

### napi_create_threadsafe_function
<!-- YAML
added: REPLACEME
-->
```C
NAPI_EXTERN napi_status
napi_create_threadsafe_function(napi_env env,
napi_value func,
void* data,
size_t argc,
napi_threadsafe_function_marshal marshal_cb,
napi_threadsafe_function_process_result
process_result_cb,
napi_threadsafe_function* result);
```
- `[in] env`: The environment that the API is invoked under.
- `[in] func`: The JavaScript function to call from another thread.
- `[in] data`: Optional data to attach to the resulting `napi_threadsafe_function`.
- `[in] context`: Optional context associated with `data`.
- `[in] argc`: Number of arguments the JavaScript function will have.
- `[in] marshal_cb`: Optional callback to convert `data` and `context` to
JavaScript function arguments. The callback will always be called on the main
thread.
- `[in] process_result_cb`: Optional callback to handle the return value and/or
exception resulting from the invocation of the JavaScript function. The callback
will always be called on the main thread.
- `[out] result`: The asynchronous thread-safe JavaScript function.
### napi_call_threadsafe_function
<!-- YAML
added: REPLACEME
-->
```C
NAPI_EXTERN napi_status
napi_call_threadsafe_function(napi_threadsafe_function func);
```

- `[in] func`: The asynchronous thread-safe JavaScript function to invoke. This
API may be called from any thread.

### napi_get_threadsafe_function_data
<!-- YAML
added: REPLACEME
-->
```C
NAPI_EXTERN napi_status
napi_get_threadsafe_function_data(napi_threadsafe_function func,
void** data);
```
- `[in] func`: The asynchronous thread-safe JavaScript function whose associated
data to retrieve.
- `[out] data`: Optional pointer to receive the data associated with the
thread-safe JavaScript function.
- `[out]: context`: Optional pointer to receive the context associated with the
thread-safe JavaScript function.
### napi_delete_threadsafe_function
<!-- YAML
added: REPLACEME
-->
```C
NAPI_EXTERN napi_status
napi_delete_threadsafe_function(napi_env env,
napi_threadsafe_function func);
```

- `[in] env`: The environment that the API is invoked under.
- `[in] func`: The asynchronous thread-safe JavaScript function to delete.

[Asynchronous Thread-safe Function Calls]: #n_api_asynchronous_thread-safe_function_calls
[Promises]: #n_api_promises
[Simple Asynchronous Operations]: #n_api_simple_asynchronous_operations
[Custom Asynchronous Operations]: #n_api_custom_asynchronous_operations
Expand Down
188 changes: 187 additions & 1 deletion src/node_api.cc
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@
#include "node_api.h"
#include "node_internals.h"

#define NAPI_VERSION 2
#define NAPI_VERSION 3

static
napi_status napi_set_last_error(napi_env env, napi_status error_code,
Expand Down Expand Up @@ -3514,3 +3514,189 @@ napi_status napi_run_script(napi_env env,
*result = v8impl::JsValueFromV8LocalValue(script_result.ToLocalChecked());
return GET_RETURN_STATUS(env);
}

struct napi_threadsafe_function__ {
uv_async_t async;
napi_ref ref;
napi_env env;
size_t argc;
void* data;
void* context;
napi_threadsafe_function_marshal marshal_cb;
napi_threadsafe_function_process_result process_result_cb;
};

static napi_value napi_threadsafe_function_error(napi_env env,
const char* message) {
napi_value result, js_message;
if (napi_create_string_utf8(env, message, NAPI_AUTO_LENGTH, &js_message) ==
napi_ok) {
if (napi_create_error(env, nullptr, js_message, &result) == napi_ok) {
return result;
}
}

napi_fatal_error("N-API thread-safe function", NAPI_AUTO_LENGTH,
(std::string("Failed to create JS error: ") +
std::string(message)).c_str(), NAPI_AUTO_LENGTH);
return nullptr;
}

static void napi_threadsafe_function_cb(uv_async_t* uv_async) {
napi_threadsafe_function async =
node::ContainerOf(&napi_threadsafe_function__::async, uv_async);
v8::HandleScope handle_scope(async->env->isolate);

napi_value js_cb;
napi_value recv;
napi_value js_result = nullptr;
napi_value exception = nullptr;
std::vector<napi_value> argv(async->argc);

napi_status status = napi_get_reference_value(async->env, async->ref, &js_cb);
if (status != napi_ok) {
exception = napi_threadsafe_function_error(async->env,
"Failed to retrieve JS callback");
goto done;
}

status = async->marshal_cb(async->env, async->data, &recv, async->argc,
argv.data());
if (status != napi_ok) {
exception = napi_threadsafe_function_error(async->env,
"Failed to marshal JS callback arguments");
goto done;
}

status = napi_make_callback(async->env, nullptr, recv, js_cb, async->argc,
argv.data(), &js_result);
if (status != napi_ok) {
if (status == napi_pending_exception) {
status = napi_get_and_clear_last_exception(async->env, &exception);
if (status != napi_ok) {
exception = napi_threadsafe_function_error(async->env,
"Failed to retrieve JS callback exception");
goto done;
}
} else {
exception = napi_threadsafe_function_error(async->env,
"Failed to call JS callback");
goto done;
}
}

done:
async->process_result_cb(async->env, async->data, exception, js_result);
}

static napi_status napi_threadsafe_function_default_marshal(napi_env env,
void* data,
napi_value* recv,
size_t argc,
napi_value* argv) {
napi_status status;
for (size_t index = 0; index < argc; index++) {
status = napi_get_undefined(env, &argv[index]);
if (status != napi_ok) {
return status;
}
}
return napi_get_global(env, recv);
}

static void napi_threadsafe_function_default_process_result(napi_env env,
void* data,
napi_value error,
napi_value result) {
if (error != nullptr) {
napi_throw(env, error);
}
}

NAPI_EXTERN napi_status
napi_create_threadsafe_function(napi_env env,
napi_value func,
void* data,
size_t argc,
napi_threadsafe_function_marshal marshal_cb,
napi_threadsafe_function_process_result
process_result_cb,
napi_threadsafe_function* result) {
CHECK_ENV(env);
CHECK_ARG(env, func);
CHECK_ARG(env, result);

napi_valuetype func_type;
napi_status status = napi_typeof(env, func, &func_type);
if (status != napi_ok) {
return status;
}

if (func_type != napi_function) {
return napi_set_last_error(env, napi_function_expected);
}

napi_threadsafe_function async = new napi_threadsafe_function__;
if (async == nullptr) {
return napi_set_last_error(env, napi_generic_failure);
}

status = napi_create_reference(env, func, 1, &async->ref);
if (status != napi_ok) {
delete async;
return status;
}

if (uv_async_init(uv_default_loop(), &async->async,
napi_threadsafe_function_cb) != 0) {
napi_delete_reference(env, async->ref);
delete async;
return napi_set_last_error(env, napi_generic_failure);
}

async->argc = argc;
async->marshal_cb = marshal_cb == nullptr ?
napi_threadsafe_function_default_marshal : marshal_cb;
async->process_result_cb =
process_result_cb == nullptr ?
napi_threadsafe_function_default_process_result : process_result_cb;
async->data = data;
async->env = env;

*result = async;
return napi_clear_last_error(env);
}

NAPI_EXTERN napi_status
napi_get_threadsafe_function_data(napi_threadsafe_function async,
void** data) {
if (data != nullptr) {
*data = async->data;
}
return napi_ok;
}

NAPI_EXTERN napi_status
napi_call_threadsafe_function(napi_threadsafe_function async) {
return uv_async_send(&async->async) == 0 ?
napi_ok : napi_generic_failure;
}

NAPI_EXTERN napi_status
napi_delete_threadsafe_function(napi_env env,
napi_threadsafe_function async) {
CHECK_ENV(env);
CHECK_ARG(env, async);

napi_status status = napi_delete_reference(env, async->ref);
if (status != napi_ok) {
return status;
}

uv_close(reinterpret_cast<uv_handle_t*>(&async->async),
[] (uv_handle_t* handle) -> void {
delete handle;
});

return napi_clear_last_error(env);
}
22 changes: 22 additions & 0 deletions src/node_api.h
Original file line number Diff line number Diff line change
Expand Up @@ -587,6 +587,28 @@ NAPI_EXTERN napi_status napi_run_script(napi_env env,
NAPI_EXTERN napi_status napi_get_uv_event_loop(napi_env env,
struct uv_loop_s** loop);

// Calling into JS from other threads
NAPI_EXTERN napi_status
napi_create_threadsafe_function(napi_env env,
napi_value func,
void* data,
size_t argc,
napi_threadsafe_function_marshal marshal_cb,
napi_threadsafe_function_process_result
process_result_cb,
napi_threadsafe_function* result);

NAPI_EXTERN napi_status
napi_call_threadsafe_function(napi_threadsafe_function func);

NAPI_EXTERN napi_status
napi_get_threadsafe_function_data(napi_threadsafe_function func,
void** data);

NAPI_EXTERN napi_status
napi_delete_threadsafe_function(napi_env env,
napi_threadsafe_function func);

EXTERN_C_END

#endif // SRC_NODE_API_H_

0 comments on commit 65e8f05

Please sign in to comment.