From 65e8f057b70c7089dd9d80d13c5670c32a6fb9bf Mon Sep 17 00:00:00 2001 From: Gabriel Schulhof Date: Wed, 20 Dec 2017 23:25:22 -0500 Subject: [PATCH] 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: https://github.com/nodejs/node/issues/13512 --- doc/api/n-api.md | 130 ++++++++++++ src/node_api.cc | 188 +++++++++++++++++- src/node_api.h | 22 ++ src/node_api_types.h | 10 + test/addons-napi/test_general/test.js | 4 +- .../test_threadsafe_function/addon.c | 20 ++ .../test_threadsafe_function/addon.h | 14 ++ .../test_threadsafe_function/binding.gyp | 12 ++ .../test_threadsafe_function/test.js | 60 ++++++ .../test_threadsafe_function/test_basic.c | 188 ++++++++++++++++++ .../test_threadsafe_function/test_empty.c | 76 +++++++ 11 files changed, 721 insertions(+), 3 deletions(-) create mode 100644 test/addons-napi/test_threadsafe_function/addon.c create mode 100644 test/addons-napi/test_threadsafe_function/addon.h create mode 100644 test/addons-napi/test_threadsafe_function/binding.gyp create mode 100644 test/addons-napi/test_threadsafe_function/test.js create mode 100644 test/addons-napi/test_threadsafe_function/test_basic.c create mode 100644 test/addons-napi/test_threadsafe_function/test_empty.c diff --git a/doc/api/n-api.md b/doc/api/n-api.md index ad6e3c7eeab02a..56eeda72a4a448 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -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++ @@ -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. @@ -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. + + +## 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 + +```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 + +```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 + +```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 + +```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 diff --git a/src/node_api.cc b/src/node_api.cc index ac0b0959b599d6..2c40ba7857e225 100644 --- a/src/node_api.cc +++ b/src/node_api.cc @@ -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, @@ -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 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(&async->async), + [] (uv_handle_t* handle) -> void { + delete handle; + }); + + return napi_clear_last_error(env); +} diff --git a/src/node_api.h b/src/node_api.h index ee0ad3518e13aa..16c430e94f65fe 100644 --- a/src/node_api.h +++ b/src/node_api.h @@ -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_ diff --git a/src/node_api_types.h b/src/node_api_types.h index 230c1f4ae3446f..3a2f4303f07595 100644 --- a/src/node_api_types.h +++ b/src/node_api_types.h @@ -19,6 +19,7 @@ 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; +typedef struct napi_threadsafe_function__ *napi_threadsafe_function; typedef enum { napi_default = 0, @@ -83,6 +84,15 @@ typedef void (*napi_async_execute_callback)(napi_env env, typedef void (*napi_async_complete_callback)(napi_env env, napi_status status, void* data); +typedef napi_status(*napi_threadsafe_function_marshal)(napi_env env, + void* data, + napi_value* recv, + size_t argc, + napi_value* argv); +typedef void(*napi_threadsafe_function_process_result)(napi_env env, + void* data, + napi_value error, + napi_value result); typedef struct { // One of utf8name or name should be NULL. diff --git a/test/addons-napi/test_general/test.js b/test/addons-napi/test_general/test.js index ee6618c8121289..5a9a7d59ca810b 100644 --- a/test/addons-napi/test_general/test.js +++ b/test/addons-napi/test_general/test.js @@ -33,8 +33,8 @@ assert.ok(test_general.testGetPrototype(baseObject) !== 'Prototypes for base and extended should be different'); // test version management functions -// expected version is currently 1 -assert.strictEqual(test_general.testGetVersion(), 2); +// expected version is currently 3 +assert.strictEqual(test_general.testGetVersion(), 3); const [ major, minor, patch, release ] = test_general.testGetNodeVersion(); assert.strictEqual(process.version.split('-')[0], diff --git a/test/addons-napi/test_threadsafe_function/addon.c b/test/addons-napi/test_threadsafe_function/addon.c new file mode 100644 index 00000000000000..54acb0fdc4e1de --- /dev/null +++ b/test/addons-napi/test_threadsafe_function/addon.c @@ -0,0 +1,20 @@ +#include +#include "addon.h" + +static void AddProperties(napi_env env, + napi_value exports, + size_t prop_count, + napi_property_descriptor* props) { + if (napi_define_properties(env, exports, prop_count, props) != napi_ok) { + napi_fatal_error("async_function", NAPI_AUTO_LENGTH, + "Failed to define properties on exports object", NAPI_AUTO_LENGTH); + } +} + +static napi_value Init(napi_env env, napi_value exports) { + InitTestBasic(env, exports, AddProperties); + InitTestEmpty(env, exports, AddProperties); + return exports; +} + +NAPI_MODULE(NODE_GYP_MODULE_NAME, Init) diff --git a/test/addons-napi/test_threadsafe_function/addon.h b/test/addons-napi/test_threadsafe_function/addon.h new file mode 100644 index 00000000000000..dff2e1226207c4 --- /dev/null +++ b/test/addons-napi/test_threadsafe_function/addon.h @@ -0,0 +1,14 @@ +#ifndef TEST_ADDONS_NAPI_TEST_THREADSAFE_FUNCTION_ADDON_H_ +#define TEST_ADDONS_NAPI_TEST_THREADSAFE_FUNCTION_ADDON_H_ + +#include + +typedef void (*AddonInit)(napi_env env, + napi_value exports, + size_t prop_count, + napi_property_descriptor* props); + +void InitTestBasic(napi_env env, napi_value exports, AddonInit init); +void InitTestEmpty(napi_env env, napi_value exports, AddonInit init); + +#endif // TEST_ADDONS_NAPI_TEST_THREADSAFE_FUNCTION_ADDON_H_ diff --git a/test/addons-napi/test_threadsafe_function/binding.gyp b/test/addons-napi/test_threadsafe_function/binding.gyp new file mode 100644 index 00000000000000..ee0c730f825005 --- /dev/null +++ b/test/addons-napi/test_threadsafe_function/binding.gyp @@ -0,0 +1,12 @@ +{ + "targets": [ + { + "target_name": "binding", + "sources": [ + "addon.c", + "test_basic.c", + "test_empty.c" + ] + } + ] +} diff --git a/test/addons-napi/test_threadsafe_function/test.js b/test/addons-napi/test_threadsafe_function/test.js new file mode 100644 index 00000000000000..899f3e9036dfc4 --- /dev/null +++ b/test/addons-napi/test_threadsafe_function/test.js @@ -0,0 +1,60 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); + +// Testing api calls for asynchronous function calls +const addon = require(`./build/${common.buildType}/binding`); + +new Promise(function testBasic(resolve, reject) { + const values = []; + const returnValues = []; + const expectedReturnValues = []; + function evaluation() { + assert.deepStrictEqual(values, [0, 1, 2, 3, 4]); + assert.deepStrictEqual(returnValues, expectedReturnValues); + } + + addon.StartThread( + (value) => { + values.push(value); + if (values.length === 5) { + setImmediate(() => { + addon.StopThread(); + resolve(evaluation); + }); + expectedReturnValues.push(new Error('Intentional error')); + throw expectedReturnValues.slice(-1)[0]; + } + expectedReturnValues.push(!!(value % 2)); + return expectedReturnValues.slice(-1)[0]; + }, + (returnValue) => (returnValues.push(returnValue))); +}).then(function(evaluation) { + evaluation(); +}).then(function() { + return new Promise(function testEmpty(resolve, reject) { + addon.StartEmptyThread(common.mustCall(() => { + setImmediate(() => { + addon.StopEmptyThread(); + resolve(); + }); + })); + }); +}).then(function() { + return new Promise(function testEmptyWithException(resolve, reject) { + addon.StartEmptyThread(() => { + const exception = new Error('Empty with exception'); + setImmediate(() => { + + // The exception thrown below will be attributed to StopEmptyThread. + try { + addon.StopEmptyThread(); + } catch (anException) { + assert.strictEqual(anException, exception); + } + resolve(); + }); + throw exception; + }); + }); +}); diff --git a/test/addons-napi/test_threadsafe_function/test_basic.c b/test/addons-napi/test_threadsafe_function/test_basic.c new file mode 100644 index 00000000000000..d6baafd185bb19 --- /dev/null +++ b/test/addons-napi/test_threadsafe_function/test_basic.c @@ -0,0 +1,188 @@ +#include +#include +#include +#include "addon.h" + +static napi_threadsafe_function async_func = NULL; + +typedef struct { + uv_thread_t thread; + uv_sem_t may_write; + int value; + napi_ref collect_results; +} TestAsyncData; + +static void process_result(napi_env env, + void* data, + napi_value error, + napi_value result) { + TestAsyncData* async = data; + napi_value collect_results; + napi_status status = napi_get_reference_value(env, async->collect_results, + &collect_results); + if (status != napi_ok) { + napi_throw_error(env, NULL, + "Failed to retrieve return value collector reference"); + return; + } + + napi_value value_to_collect = + (error == NULL ? result == NULL ? NULL : result : error); + + if (value_to_collect != NULL) { + status = napi_call_function(env, collect_results, collect_results, 1, + &value_to_collect, NULL); + if (status != napi_ok) { + napi_throw_error(env, NULL, "Failed to call return value collector"); + } + } +} + +static napi_status marshal_data(napi_env env, + void* data, + napi_value* recv, + size_t argc, + napi_value* argv) { + int value; + TestAsyncData* async = data; + napi_status status = napi_create_object(env, recv); + if (status != napi_ok) { + return status; + } + + value = async->value; + uv_sem_post(&async->may_write); + + status = napi_create_int32(env, value, argv); + if (status != napi_ok) { + return status; + } + + return napi_ok; +} + +static void BasicTestThread(void* thread_data) { + napi_threadsafe_function async = thread_data; + void* data = NULL; + TestAsyncData* test_data = NULL; + int value; + napi_status status = napi_get_threadsafe_function_data(async, &data); + if (status != napi_ok) { + napi_fatal_error("BasicTestThread", NAPI_AUTO_LENGTH, + "Failed to retrieve async function data", NAPI_AUTO_LENGTH); + } + test_data = (TestAsyncData*)data; + + for (value = 0; value < 5; value++) { + uv_sem_wait(&test_data->may_write); + test_data->value = value; + status = napi_call_threadsafe_function(async); + if (status != napi_ok) { + napi_fatal_error("BasicTestThread", NAPI_AUTO_LENGTH, + "Failed to call async function", NAPI_AUTO_LENGTH); + } + } +} + +static napi_value StartThread(napi_env env, napi_callback_info info) { + const char* error; + size_t argc = 2; + napi_value argv[2]; + napi_status status = + napi_get_cb_info(env, info, &argc, argv, NULL, NULL); + if (status != napi_ok) { + error = "Failed to retrieve JS callback"; + goto throw; + } + + if (async_func) { + error = "There is already an async function in place"; + goto throw; + } + + TestAsyncData* async_data = malloc(sizeof(*async_data)); + if (async_data == NULL) { + error = "Failed to allocate memory for test data"; + goto throw; + } + async_data->value = -1; + + status = napi_create_reference(env, argv[1], 1, &async_data->collect_results); + if (status != napi_ok) { + error = "Failed to create reference to function that collects the " + "JS callback return values"; + goto free_data; + } + + status = napi_create_threadsafe_function(env, argv[0], async_data, 1, + marshal_data, process_result, &async_func); + if (status != napi_ok) { + error = "Failed to create async function"; + goto delete_reference; + } + + if (uv_sem_init(&async_data->may_write, 1) != 0) { + error = "Failed to initialize write sem"; + goto delete_async_function; + } + + if (uv_thread_create(&async_data->thread, BasicTestThread, async_func) != 0) { + error = "Failed to start thread"; + goto destroy_semaphore; + } + + goto done; + +destroy_semaphore: + uv_sem_destroy(&async_data->may_write); +delete_async_function: + napi_delete_threadsafe_function(env, async_func); +delete_reference: + napi_delete_reference(env, async_data->collect_results); +free_data: + free(async_data); +throw: + napi_throw_error(env, NULL, error); +done: + return NULL; +} + +static napi_value StopThread(napi_env env, napi_callback_info info) { + if (async_func == NULL) { + napi_throw_error(env, NULL, + "There is no asynchronous function currently open"); + return NULL; + } + + void* data; + napi_status status = + napi_get_threadsafe_function_data(async_func, &data); + if (status != napi_ok) { + napi_throw_error(env, NULL, "Failed to retrieve async function data"); + return NULL; + } + TestAsyncData* test_data = data; + + if (uv_thread_join(&test_data->thread) != 0) { + napi_throw_error(env, NULL, "Failed to stop test thread"); + return NULL; + } + + status = napi_delete_threadsafe_function(env, async_func); + if (status != napi_ok) { + napi_throw_error(env, NULL, "Failed to delete async function"); + } + + uv_sem_destroy(&test_data->may_write); + free(test_data); + async_func = NULL; + return NULL; +} + +void InitTestBasic(napi_env env, napi_value exports, AddonInit init) { + napi_property_descriptor props[] = { + {"StartThread", NULL, StartThread, NULL, NULL, NULL, napi_enumerable, NULL}, + {"StopThread", NULL, StopThread, NULL, NULL, NULL, napi_enumerable, NULL} + }; + init(env, exports, sizeof(props)/sizeof(props[0]), props); +} diff --git a/test/addons-napi/test_threadsafe_function/test_empty.c b/test/addons-napi/test_threadsafe_function/test_empty.c new file mode 100644 index 00000000000000..8ac288c080266e --- /dev/null +++ b/test/addons-napi/test_threadsafe_function/test_empty.c @@ -0,0 +1,76 @@ +#include +#include +#include +#include "addon.h" + +static napi_threadsafe_function async_func = NULL; +static uv_thread_t thread; + +static void EmptyTestThread(void* thread_data) { + if (napi_call_threadsafe_function((napi_threadsafe_function)thread_data) != + napi_ok) { + napi_fatal_error("EmptyTestThread", NAPI_AUTO_LENGTH, + "Failed to call async function", NAPI_AUTO_LENGTH); + } +} + +static napi_value StartEmptyThread(napi_env env, napi_callback_info info) { + size_t argc = 1; + napi_value js_cb; + napi_status status = + napi_get_cb_info(env, info, &argc, &js_cb, NULL, NULL); + if (status != napi_ok) { + napi_throw_error(env, NULL, "Failed to retrieve JS callback"); + return NULL; + } + + if (async_func != NULL) { + napi_throw_error(env, NULL, "There is already an async function in place"); + return NULL; + } + + status = napi_create_threadsafe_function(env, js_cb, NULL, 0, NULL, NULL, + &async_func); + if (status != napi_ok) { + napi_throw_error(env, NULL, "Failed to create async function"); + return NULL; + } + + if (uv_thread_create(&thread, EmptyTestThread, async_func) != 0) { + napi_delete_threadsafe_function(env, async_func); + napi_throw_error(env, NULL, "Failed to start thread"); + return NULL; + } + + return NULL; +} + +static napi_value StopEmptyThread(napi_env env, napi_callback_info info) { + if (async_func == NULL) { + napi_throw_error(env, NULL, + "There is no asynchronous function currently open"); + return NULL; + } + + if (uv_thread_join(&thread) != 0) { + napi_throw_error(env, NULL, "Failed to stop test thread"); + return NULL; + } + + if (napi_delete_threadsafe_function(env, async_func) != napi_ok) { + napi_throw_error(env, NULL, "Failed to delete async function"); + } + + async_func = NULL; + return NULL; +} + +void InitTestEmpty(napi_env env, napi_value exports, AddonInit init) { + napi_property_descriptor props[] = { + {"StartEmptyThread", NULL, StartEmptyThread, NULL, NULL, NULL, + napi_enumerable, NULL}, + {"StopEmptyThread", NULL, StopEmptyThread, NULL, NULL, NULL, + napi_enumerable, NULL} + }; + init(env, exports, sizeof(props)/sizeof(props[0]), props); +}