Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

n-api: add API for asynchronous functions #17809

Closed
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm not sure if these headers should be italicized or not. It's inconsistent throughout the file.

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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This function and napi_delete_threadsafe_function() are poorly named, IMO. The "threadsafe" suggests they're, well, thread-safe, but they're not - no locking, no synchronization, nothing.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is it not sufficient that the void* is established before any thread is started, and is read-only after that? Perhaps I need to add this recommendation to the documentation.

As for the delete, I guess my intention was that it happen after all threads using the napi_threadsafe_function have been stopped. That is, one would call uv_thread_join() fir all threads first, and then napi_delete_threadsafe_function().

I guess I was also leaning on documentation whereby napi_env must only be used on the main thread to indicate that napi_delete_threadsafe_function() must only be called from the main thread.

<!-- 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_
Loading