From 9decfb15215978b73094ffb7a4bdf2a0da010258 Mon Sep 17 00:00:00 2001 From: "taylor.woll" Date: Tue, 28 Mar 2017 02:06:58 -0700 Subject: [PATCH] n-api: implement async helper methods Based on the async methods we had in abi-stable-node before the napi feature landed in node/master. Changed this set of APIs to handle error cases and removed a lot of the extra methods we had for setting all the pieces of napi_work opting instead to pass all of those as arguments to napi_create_async_work as none of those parameters are optional except for the complete callback, anyway. Renamed the napi_work struct to napi_async_work and replace the struct itself with a class which can better encapsulate the object lifetime and uv_work_t that we're trying to wrap anyway. Added a napi_async_callback type for the async helper callbacks instead of taking raw function pointers and make this callback take a napi_env parameter as well as the void* data it was already taking. Call the complete handler for the async work item with a napi_status code translated from the uvlib error code. The execute callback is required for napi_create_async_work, though complete callback is still optional. Also added some async unit tests for addons-napi based on the addons/async_hello_world test. PR-URL: https://github.com/nodejs/node/pull/12250 Reviewed-By: Anna Henningsen Reviewed-By: Michael Dawson Reviewed-By: Timothy Gu Reviewed-By: Hitesh Kanwathirtha --- src/node_api.cc | 137 +++++++++++++++++++++- src/node_api.h | 15 +++ src/node_api_types.h | 47 ++++---- test/addons-napi/test_async/binding.gyp | 8 ++ test/addons-napi/test_async/test.js | 10 ++ test/addons-napi/test_async/test_async.cc | 122 +++++++++++++++++++ 6 files changed, 318 insertions(+), 21 deletions(-) create mode 100644 test/addons-napi/test_async/binding.gyp create mode 100644 test/addons-napi/test_async/test.js create mode 100644 test/addons-napi/test_async/test_async.cc diff --git a/src/node_api.cc b/src/node_api.cc index 418a18c8a06efd..d61119bbd6fb03 100644 --- a/src/node_api.cc +++ b/src/node_api.cc @@ -15,6 +15,7 @@ #include #include #include "node_api.h" +#include "env-inl.h" napi_status napi_set_last_error(napi_env env, napi_status error_code, uint32_t engine_error_code = 0, @@ -705,7 +706,8 @@ const char* error_messages[] = {nullptr, "A boolean was expected", "An array was expected", "Unknown failure", - "An exception is pending"}; + "An exception is pending", + "The async work item was cancelled"}; void napi_clear_last_error(napi_env env) { env->last_error.error_code = napi_ok; @@ -2574,3 +2576,136 @@ napi_status napi_get_typedarray_info(napi_env env, return GET_RETURN_STATUS(env); } + +namespace uvimpl { + +napi_status ConvertUVErrorCode(int code) { + switch (code) { + case 0: + return napi_ok; + case UV_EINVAL: + return napi_invalid_arg; + case UV_ECANCELED: + return napi_cancelled; + } + + return napi_generic_failure; +} + +// Wrapper around uv_work_t which calls user-provided callbacks. +class Work { + private: + explicit Work(napi_env env, + napi_async_execute_callback execute = nullptr, + napi_async_complete_callback complete = nullptr, + void* data = nullptr) + : _env(env), + _data(data), + _execute(execute), + _complete(complete) { + _request.data = this; + } + + ~Work() { } + + public: + static Work* New(napi_env env, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void* data) { + return new Work(env, execute, complete, data); + } + + static void Delete(Work* work) { + delete work; + } + + static void ExecuteCallback(uv_work_t* req) { + Work* work = static_cast(req->data); + work->_execute(work->_env, work->_data); + } + + static void CompleteCallback(uv_work_t* req, int status) { + Work* work = static_cast(req->data); + + if (work->_complete != nullptr) { + work->_complete(work->_env, ConvertUVErrorCode(status), work->_data); + } + } + + uv_work_t* Request() { + return &_request; + } + + private: + napi_env _env; + void* _data; + uv_work_t _request; + napi_async_execute_callback _execute; + napi_async_complete_callback _complete; +}; + +} // end of namespace uvimpl + +#define CALL_UV(env, condition) \ + do { \ + int result = (condition); \ + napi_status status = uvimpl::ConvertUVErrorCode(result); \ + if (status != napi_ok) { \ + return napi_set_last_error(env, status, result); \ + } \ + } while (0) + +napi_status napi_create_async_work(napi_env env, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void* data, + napi_async_work* result) { + CHECK_ENV(env); + CHECK_ARG(env, execute); + CHECK_ARG(env, result); + + uvimpl::Work* work = uvimpl::Work::New(env, execute, complete, data); + + *result = reinterpret_cast(work); + + return napi_ok; +} + +napi_status napi_delete_async_work(napi_env env, napi_async_work work) { + CHECK_ENV(env); + CHECK_ARG(env, work); + + uvimpl::Work::Delete(reinterpret_cast(work)); + + return napi_ok; +} + +napi_status napi_queue_async_work(napi_env env, napi_async_work work) { + CHECK_ENV(env); + CHECK_ARG(env, work); + + // Consider: Encapsulate the uv_loop_t into an opaque pointer parameter + uv_loop_t* event_loop = + node::Environment::GetCurrent(env->isolate)->event_loop(); + + uvimpl::Work* w = reinterpret_cast(work); + + CALL_UV(env, uv_queue_work(event_loop, + w->Request(), + uvimpl::Work::ExecuteCallback, + uvimpl::Work::CompleteCallback)); + + return napi_ok; +} + +napi_status napi_cancel_async_work(napi_env env, napi_async_work work) { + CHECK_ENV(env); + CHECK_ARG(env, work); + + uvimpl::Work* w = reinterpret_cast(work); + + CALL_UV(env, uv_cancel(reinterpret_cast(w->Request()))); + + return napi_ok; +} diff --git a/src/node_api.h b/src/node_api.h index c232d9aa039d13..8791954a377052 100644 --- a/src/node_api.h +++ b/src/node_api.h @@ -457,6 +457,21 @@ NAPI_EXTERN napi_status napi_get_typedarray_info(napi_env env, void** data, napi_value* arraybuffer, size_t* byte_offset); + +// Methods to manage simple async operations +NAPI_EXTERN +napi_status napi_create_async_work(napi_env env, + napi_async_execute_callback execute, + napi_async_complete_callback complete, + void* data, + napi_async_work* result); +NAPI_EXTERN napi_status napi_delete_async_work(napi_env env, + napi_async_work work); +NAPI_EXTERN napi_status napi_queue_async_work(napi_env env, + napi_async_work work); +NAPI_EXTERN napi_status napi_cancel_async_work(napi_env env, + napi_async_work work); + EXTERN_C_END #endif // SRC_NODE_API_H__ diff --git a/src/node_api_types.h b/src/node_api_types.h index f439cce1f9003b..4bf1b8263139d8 100644 --- a/src/node_api_types.h +++ b/src/node_api_types.h @@ -16,12 +16,7 @@ typedef struct napi_ref__ *napi_ref; typedef struct napi_handle_scope__ *napi_handle_scope; typedef struct napi_escapable_handle_scope__ *napi_escapable_handle_scope; typedef struct napi_callback_info__ *napi_callback_info; - -typedef napi_value (*napi_callback)(napi_env env, - napi_callback_info info); -typedef void (*napi_finalize)(napi_env env, - void* finalize_data, - void* finalize_hint); +typedef struct napi_async_work__ *napi_async_work; typedef enum { napi_default = 0, @@ -34,20 +29,6 @@ typedef enum { napi_static = 1 << 10, } napi_property_attributes; -typedef struct { - // One of utf8name or name should be NULL. - const char* utf8name; - napi_value name; - - napi_callback method; - napi_callback getter; - napi_callback setter; - napi_value value; - - napi_property_attributes attributes; - void* data; -} napi_property_descriptor; - typedef enum { // ES6 types (corresponds to typeof) napi_undefined, @@ -85,9 +66,35 @@ typedef enum { napi_array_expected, napi_generic_failure, napi_pending_exception, + napi_cancelled, napi_status_last } napi_status; +typedef napi_value (*napi_callback)(napi_env env, + napi_callback_info info); +typedef void (*napi_finalize)(napi_env env, + void* finalize_data, + void* finalize_hint); +typedef void (*napi_async_execute_callback)(napi_env env, + void* data); +typedef void (*napi_async_complete_callback)(napi_env env, + napi_status status, + void* data); + +typedef struct { + // One of utf8name or name should be NULL. + const char* utf8name; + napi_value name; + + napi_callback method; + napi_callback getter; + napi_callback setter; + napi_value value; + + napi_property_attributes attributes; + void* data; +} napi_property_descriptor; + typedef struct { const char* error_message; void* engine_reserved; diff --git a/test/addons-napi/test_async/binding.gyp b/test/addons-napi/test_async/binding.gyp new file mode 100644 index 00000000000000..cf8beb70c68e78 --- /dev/null +++ b/test/addons-napi/test_async/binding.gyp @@ -0,0 +1,8 @@ +{ + "targets": [ + { + "target_name": "test_async", + "sources": [ "test_async.cc" ] + } + ] +} diff --git a/test/addons-napi/test_async/test.js b/test/addons-napi/test_async/test.js new file mode 100644 index 00000000000000..0bfd955ac77006 --- /dev/null +++ b/test/addons-napi/test_async/test.js @@ -0,0 +1,10 @@ +'use strict'; +const common = require('../../common'); +const assert = require('assert'); +const test_async = require(`./build/${common.buildType}/test_async`); + +test_async(5, common.mustCall(function(err, val) { + assert.strictEqual(err, null); + assert.strictEqual(val, 10); + process.nextTick(common.mustCall(function() {})); +})); diff --git a/test/addons-napi/test_async/test_async.cc b/test/addons-napi/test_async/test_async.cc new file mode 100644 index 00000000000000..bc0af66d26ec61 --- /dev/null +++ b/test/addons-napi/test_async/test_async.cc @@ -0,0 +1,122 @@ +#include +#include "../common.h" + +#if defined _WIN32 +#include +#else +#include +#endif + +typedef struct { + int32_t _input; + int32_t _output; + napi_ref _callback; + napi_async_work _request; +} carrier; + +carrier the_carrier; + +struct AutoHandleScope { + explicit AutoHandleScope(napi_env env) + : _env(env), + _scope(nullptr) { + napi_open_handle_scope(_env, &_scope); + } + ~AutoHandleScope() { + napi_close_handle_scope(_env, _scope); + } + private: + AutoHandleScope() { } + + napi_env _env; + napi_handle_scope _scope; +}; + +void Execute(napi_env env, void* data) { +#if defined _WIN32 + Sleep(1000); +#else + sleep(1); +#endif + carrier* c = static_cast(data); + + if (c != &the_carrier) { + napi_throw_type_error(env, "Wrong data parameter to Execute."); + return; + } + + c->_output = c->_input * 2; +} + +void Complete(napi_env env, napi_status status, void* data) { + AutoHandleScope scope(env); + carrier* c = static_cast(data); + + if (c != &the_carrier) { + napi_throw_type_error(env, "Wrong data parameter to Complete."); + return; + } + + if (status != napi_ok) { + napi_throw_type_error(env, "Execute callback failed."); + return; + } + + napi_value argv[2]; + + NAPI_CALL_RETURN_VOID(env, napi_get_null(env, &argv[0])); + NAPI_CALL_RETURN_VOID(env, napi_create_number(env, c->_output, &argv[1])); + napi_value callback; + NAPI_CALL_RETURN_VOID(env, + napi_get_reference_value(env, c->_callback, &callback)); + napi_value global; + NAPI_CALL_RETURN_VOID(env, napi_get_global(env, &global)); + + napi_value result; + NAPI_CALL_RETURN_VOID(env, + napi_call_function(env, global, callback, 2, argv, &result)); + + NAPI_CALL_RETURN_VOID(env, napi_delete_reference(env, c->_callback)); + NAPI_CALL_RETURN_VOID(env, napi_delete_async_work(env, c->_request)); +} + +napi_value Test(napi_env env, napi_callback_info info) { + size_t argc = 2; + napi_value argv[2]; + napi_value _this; + void* data; + NAPI_CALL(env, + napi_get_cb_info(env, info, &argc, argv, &_this, &data)); + NAPI_ASSERT(env, argc >= 2, "Not enough arguments, expected 2."); + + napi_valuetype t; + NAPI_CALL(env, napi_typeof(env, argv[0], &t)); + NAPI_ASSERT(env, t == napi_number, + "Wrong first argument, integer expected."); + NAPI_CALL(env, napi_typeof(env, argv[1], &t)); + NAPI_ASSERT(env, t == napi_function, + "Wrong second argument, function expected."); + + the_carrier._output = 0; + + NAPI_CALL(env, + napi_get_value_int32(env, argv[0], &the_carrier._input)); + NAPI_CALL(env, + napi_create_reference(env, argv[1], 1, &the_carrier._callback)); + NAPI_CALL(env, napi_create_async_work( + env, Execute, Complete, &the_carrier, &the_carrier._request)); + NAPI_CALL(env, + napi_queue_async_work(env, the_carrier._request)); + + return nullptr; +} + +void Init(napi_env env, napi_value exports, napi_value module, void* priv) { + napi_value test; + NAPI_CALL_RETURN_VOID(env, + napi_create_function(env, "Test", Test, nullptr, &test)); + NAPI_CALL_RETURN_VOID(env, + napi_set_named_property(env, module, "exports", test)); +} + +NAPI_MODULE(addon, Init)