From 278a2d069f97e7fdff5892eefc98d85f38153c94 Mon Sep 17 00:00:00 2001 From: Gabriel Schulhof Date: Thu, 24 Aug 2017 13:33:26 +0300 Subject: [PATCH] n-api: implement promise Promise is implemented as a pair of objects. `napi_create_promise()` returns both a JavaScript promise and a newly allocated "deferred" in its out-params. The deferred is linked to the promise such that the deferred can be passed to `napi_resolve_deferred()` or `napi_reject_deferred()` to reject/resolve the promise. `napi_is_promise()` can be used to check if a `napi_value` is a native promise - that is, a promise created by the underlying engine, rather than a pure JS implementation of a promise. Backport-PR-URL: https://github.com/nodejs/node/pull/19447 PR-URL: https://github.com/nodejs/node/pull/14365 Fixes: https://github.com/nodejs/abi-stable-node/issues/242 Reviewed-By: Anna Henningsen Reviewed-By: James M Snell Reviewed-By: Benjamin Gruenbaum Reviewed-By: Timothy Gu --- doc/api/n-api.md | 138 +++++++++++++++++++ src/node_api.cc | 78 +++++++++++ src/node_api.h | 14 ++ src/node_api_types.h | 1 + test/addons-napi/test_promise/binding.gyp | 8 ++ test/addons-napi/test_promise/test.js | 60 ++++++++ test/addons-napi/test_promise/test_promise.c | 60 ++++++++ 7 files changed, 359 insertions(+) create mode 100644 test/addons-napi/test_promise/binding.gyp create mode 100644 test/addons-napi/test_promise/test.js create mode 100644 test/addons-napi/test_promise/test_promise.c diff --git a/doc/api/n-api.md b/doc/api/n-api.md index 89aa08a15198ce..12aad7583f6d93 100644 --- a/doc/api/n-api.md +++ b/doc/api/n-api.md @@ -42,6 +42,7 @@ The documentation for N-API is structured as follows: * [Working with JavaScript Functions][] * [Object Wrap][] * [Asynchronous Operations][] +* [Promises][] 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++ @@ -3395,6 +3396,142 @@ support it: +## Promises + +N-API provides facilities for creating `Promise` objects as described in +[Section 25.4][] of the ECMA specification. It implements promises as a pair of +objects. When a promise is created by `napi_create_promise()`, a "deferred" +object is created and returned alongside the `Promise`. The deferred object is +bound to the created `Promise` and is the only means to resolve or reject the +`Promise` using `napi_resolve_deferred()` or `napi_reject_deferred()`. The +deferred object that is created by `napi_create_promise()` is freed by +`napi_resolve_deferred()` or `napi_reject_deferred()`. The `Promise` object may +be returned to JavaScript where it can be used in the usual fashion. + +For example, to create a promise and pass it to an asynchronous worker: +```c +napi_deferred deferred; +napi_value promise; +napi_status status; + +// Create the promise. +status = napi_create_promise(env, &deferred, &promise); +if (status != napi_ok) return NULL; + +// Pass the deferred to a function that performs an asynchronous action. +do_something_asynchronous(deferred); + +// Return the promise to JS +return promise; +``` + +The above function `do_something_asynchronous()` would perform its asynchronous +action and then it would resolve or reject the deferred, thereby concluding the +promise and freeing the deferred: +```c +napi_deferred deferred; +napi_value undefined; +napi_status status; + +// Create a value with which to conclude the deferred. +status = napi_get_undefined(env, &undefined); +if (status != napi_ok) return NULL; + +// Resolve or reject the promise associated with the deferred depending on +// whether the asynchronous action succeeded. +if (asynchronous_action_succeeded) { + status = napi_resolve_deferred(env, deferred, undefined); +} else { + status = napi_reject_deferred(env, deferred, undefined); +} +if (status != napi_ok) return NULL; + +// At this point the deferred has been freed, so we should assign NULL to it. +deferred = NULL; +``` + +### napi_create_promise + +```C +NAPI_EXTERN napi_status napi_create_promise(napi_env env, + napi_deferred* deferred, + napi_value* promise); +``` + +- `[in] env`: The environment that the API is invoked under. +- `[out] deferred`: A newly created deferred object which can later be passed to +`napi_resolve_deferred()` or `napi_reject_deferred()` to resolve resp. reject +the associated promise. +- `[out] promise`: The JavaScript promise associated with the deferred object. + +Returns `napi_ok` if the API succeeded. + +This API creates a deferred object and a JavaScript promise. + +### napi_resolve_deferred + +```C +NAPI_EXTERN napi_status napi_resolve_deferred(napi_env env, + napi_deferred deferred, + napi_value resolution); +``` + +- `[in] env`: The environment that the API is invoked under. +- `[in] deferred`: The deferred object whose associated promise to resolve. +- `[in] resolution`: The value with which to resolve the promise. + +This API resolves a JavaScript promise by way of the deferred object +with which it is associated. Thus, it can only be used to resolve JavaScript +promises for which the corresponding deferred object is available. This +effectively means that the promise must have been created using +`napi_create_promise()` and the deferred object returned from that call must +have been retained in order to be passed to this API. + +The deferred object is freed upon successful completion. + +### napi_reject_deferred + +```C +NAPI_EXTERN napi_status napi_reject_deferred(napi_env env, + napi_deferred deferred, + napi_value rejection); +``` + +- `[in] env`: The environment that the API is invoked under. +- `[in] deferred`: The deferred object whose associated promise to resolve. +- `[in] rejection`: The value with which to reject the promise. + +This API rejects a JavaScript promise by way of the deferred object +with which it is associated. Thus, it can only be used to reject JavaScript +promises for which the corresponding deferred object is available. This +effectively means that the promise must have been created using +`napi_create_promise()` and the deferred object returned from that call must +have been retained in order to be passed to this API. + +The deferred object is freed upon successful completion. + +### napi_is_promise + +```C +NAPI_EXTERN napi_status napi_is_promise(napi_env env, + napi_value promise, + bool* is_promise); +``` + +- `[in] env`: The environment that the API is invoked under. +- `[in] promise`: The promise to examine +- `[out] is_promise`: Flag indicating whether `promise` is a native promise +object - that is, a promise object created by the underlying engine. + +[Promises]: #n_api_promises [Asynchronous Operations]: #n_api_asynchronous_operations [Basic N-API Data Types]: #n_api_basic_n_api_data_types [ECMAScript Language Specification]: https://tc39.github.io/ecma262/ @@ -3406,6 +3543,7 @@ support it: [Section 9.1.6]: https://tc39.github.io/ecma262/#sec-ordinary-object-internal-methods-and-internal-slots-defineownproperty-p-desc [Section 12.5.5]: https://tc39.github.io/ecma262/#sec-typeof-operator [Section 24.3]: https://tc39.github.io/ecma262/#sec-dataview-objects +[Section 25.4]: https://tc39.github.io/ecma262/#sec-promise-objects [Working with JavaScript Functions]: #n_api_working_with_javascript_functions [Working with JavaScript Properties]: #n_api_working_with_javascript_properties [Working with JavaScript Values]: #n_api_working_with_javascript_values diff --git a/src/node_api.cc b/src/node_api.cc index db66ea0bab4550..fd31817e4a8f71 100644 --- a/src/node_api.cc +++ b/src/node_api.cc @@ -223,6 +223,14 @@ V8EscapableHandleScopeFromJsEscapableHandleScope( static_assert(sizeof(v8::Local) == sizeof(napi_value), "Cannot convert between v8::Local and napi_value"); +napi_deferred JsDeferredFromV8Persistent(v8::Persistent* local) { + return reinterpret_cast(local); +} + +v8::Persistent* V8PersistentFromJsDeferred(napi_deferred local) { + return reinterpret_cast*>(local); +} + napi_value JsValueFromV8LocalValue(v8::Local local) { return reinterpret_cast(*local); } @@ -779,6 +787,33 @@ napi_status Unwrap(napi_env env, return napi_ok; } +napi_status ConcludeDeferred(napi_env env, + napi_deferred deferred, + napi_value result, + bool is_resolved) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, result); + + v8::Local context = env->isolate->GetCurrentContext(); + v8::Persistent* deferred_ref = + V8PersistentFromJsDeferred(deferred); + v8::Local v8_deferred = + v8::Local::New(env->isolate, *deferred_ref); + + auto v8_resolver = v8::Local::Cast(v8_deferred); + + v8::Maybe success = is_resolved ? + v8_resolver->Resolve(context, v8impl::V8LocalValueFromJsValue(result)) : + v8_resolver->Reject(context, v8impl::V8LocalValueFromJsValue(result)); + + deferred_ref->Reset(); + delete deferred_ref; + + RETURN_STATUS_IF_FALSE(env, success.FromMaybe(false), napi_generic_failure); + + return GET_RETURN_STATUS(env); +} + } // end of namespace v8impl // Intercepts the Node-V8 module registration callback. Converts parameters @@ -3335,3 +3370,46 @@ napi_status napi_cancel_async_work(napi_env env, napi_async_work work) { return napi_clear_last_error(env); } + +NAPI_EXTERN napi_status napi_create_promise(napi_env env, + napi_deferred* deferred, + napi_value* promise) { + NAPI_PREAMBLE(env); + CHECK_ARG(env, deferred); + CHECK_ARG(env, promise); + + auto maybe = v8::Promise::Resolver::New(env->isolate->GetCurrentContext()); + CHECK_MAYBE_EMPTY(env, maybe, napi_generic_failure); + + auto v8_resolver = maybe.ToLocalChecked(); + auto v8_deferred = new v8::Persistent(); + v8_deferred->Reset(env->isolate, v8_resolver); + + *deferred = v8impl::JsDeferredFromV8Persistent(v8_deferred); + *promise = v8impl::JsValueFromV8LocalValue(v8_resolver->GetPromise()); + return GET_RETURN_STATUS(env); +} + +NAPI_EXTERN napi_status napi_resolve_deferred(napi_env env, + napi_deferred deferred, + napi_value resolution) { + return v8impl::ConcludeDeferred(env, deferred, resolution, true); +} + +NAPI_EXTERN napi_status napi_reject_deferred(napi_env env, + napi_deferred deferred, + napi_value resolution) { + return v8impl::ConcludeDeferred(env, deferred, resolution, false); +} + +NAPI_EXTERN napi_status napi_is_promise(napi_env env, + napi_value promise, + bool* is_promise) { + CHECK_ENV(env); + CHECK_ARG(env, promise); + CHECK_ARG(env, is_promise); + + *is_promise = v8impl::V8LocalValueFromJsValue(promise)->IsPromise(); + + return napi_clear_last_error(env); +} diff --git a/src/node_api.h b/src/node_api.h index e52e2016d733b9..6a4b2941879ff0 100644 --- a/src/node_api.h +++ b/src/node_api.h @@ -543,6 +543,20 @@ NAPI_EXTERN napi_status napi_get_node_version(napi_env env, const napi_node_version** version); +// Promises +NAPI_EXTERN napi_status napi_create_promise(napi_env env, + napi_deferred* deferred, + napi_value* promise); +NAPI_EXTERN napi_status napi_resolve_deferred(napi_env env, + napi_deferred deferred, + napi_value resolution); +NAPI_EXTERN napi_status napi_reject_deferred(napi_env env, + napi_deferred deferred, + napi_value rejection); +NAPI_EXTERN napi_status napi_is_promise(napi_env env, + napi_value promise, + bool* is_promise); + EXTERN_C_END #endif // SRC_NODE_API_H_ diff --git a/src/node_api_types.h b/src/node_api_types.h index 0bdc377c8fc6a0..ac8482bf9dcf9d 100644 --- a/src/node_api_types.h +++ b/src/node_api_types.h @@ -17,6 +17,7 @@ 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 struct napi_async_work__ *napi_async_work; +typedef struct napi_deferred__ *napi_deferred; typedef enum { napi_default = 0, diff --git a/test/addons-napi/test_promise/binding.gyp b/test/addons-napi/test_promise/binding.gyp new file mode 100644 index 00000000000000..bf266f93db74be --- /dev/null +++ b/test/addons-napi/test_promise/binding.gyp @@ -0,0 +1,8 @@ +{ + "targets": [ + { + "target_name": "test_promise", + "sources": [ "test_promise.c" ] + } + ] +} diff --git a/test/addons-napi/test_promise/test.js b/test/addons-napi/test_promise/test.js new file mode 100644 index 00000000000000..4c2a2e5e76c9fb --- /dev/null +++ b/test/addons-napi/test_promise/test.js @@ -0,0 +1,60 @@ +'use strict'; + +const common = require('../../common'); +const test_promise = require(`./build/${common.buildType}/test_promise`); +const assert = require('assert'); + +let expected_result, promise; + +// A resolution +expected_result = 42; +promise = test_promise.createPromise(); +promise.then( + common.mustCall(function(result) { + assert.strictEqual(result, expected_result, + 'promise resolved as expected'); + }), + common.mustNotCall()); +test_promise.concludeCurrentPromise(expected_result, true); + +// A rejection +expected_result = 'It\'s not you, it\'s me.'; +promise = test_promise.createPromise(); +promise.then( + common.mustNotCall(), + common.mustCall(function(result) { + assert.strictEqual(result, expected_result, + 'promise rejected as expected'); + })); +test_promise.concludeCurrentPromise(expected_result, false); + +// Chaining +promise = test_promise.createPromise(); +promise.then( + common.mustCall(function(result) { + assert.strictEqual(result, 'chained answer', + 'resolving with a promise chains properly'); + }), + common.mustNotCall()); +test_promise.concludeCurrentPromise(Promise.resolve('chained answer'), true); + +assert.strictEqual(test_promise.isPromise(promise), true, + 'natively created promise is recognized as a promise'); + +assert.strictEqual(test_promise.isPromise(Promise.reject(-1)), true, + 'Promise created with JS is recognized as a promise'); + +assert.strictEqual(test_promise.isPromise(2.4), false, + 'Number is recognized as not a promise'); + +assert.strictEqual(test_promise.isPromise('I promise!'), false, + 'String is recognized as not a promise'); + +assert.strictEqual(test_promise.isPromise(undefined), false, + 'undefined is recognized as not a promise'); + +assert.strictEqual(test_promise.isPromise(null), false, + 'null is recognized as not a promise'); + +assert.strictEqual(test_promise.isPromise({}), false, + 'an object is recognized as not a promise'); diff --git a/test/addons-napi/test_promise/test_promise.c b/test/addons-napi/test_promise/test_promise.c new file mode 100644 index 00000000000000..dc617f4592e520 --- /dev/null +++ b/test/addons-napi/test_promise/test_promise.c @@ -0,0 +1,60 @@ +#include +#include "../common.h" + +napi_deferred deferred = NULL; + +napi_value createPromise(napi_env env, napi_callback_info info) { + napi_value promise; + + // We do not overwrite an existing deferred. + if (deferred != NULL) { + return NULL; + } + + NAPI_CALL(env, napi_create_promise(env, &deferred, &promise)); + + return promise; +} + +napi_value concludeCurrentPromise(napi_env env, napi_callback_info info) { + napi_value argv[2]; + size_t argc = 2; + bool resolution; + + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, argv, NULL, NULL)); + NAPI_CALL(env, napi_get_value_bool(env, argv[1], &resolution)); + if (resolution) { + NAPI_CALL(env, napi_resolve_deferred(env, deferred, argv[0])); + } else { + NAPI_CALL(env, napi_reject_deferred(env, deferred, argv[0])); + } + + deferred = NULL; + + return NULL; +} + +napi_value isPromise(napi_env env, napi_callback_info info) { + napi_value promise, result; + size_t argc = 1; + bool is_promise; + + NAPI_CALL(env, napi_get_cb_info(env, info, &argc, &promise, NULL, NULL)); + NAPI_CALL(env, napi_is_promise(env, promise, &is_promise)); + NAPI_CALL(env, napi_get_boolean(env, is_promise, &result)); + + return result; +} + +void Init(napi_env env, napi_value exports, napi_value module, void* priv) { + napi_property_descriptor descriptors[] = { + DECLARE_NAPI_PROPERTY("createPromise", createPromise), + DECLARE_NAPI_PROPERTY("concludeCurrentPromise", concludeCurrentPromise), + DECLARE_NAPI_PROPERTY("isPromise", isPromise), + }; + + NAPI_CALL_RETURN_VOID(env, napi_define_properties( + env, exports, sizeof(descriptors) / sizeof(*descriptors), descriptors)); +} + +NAPI_MODULE(addon, Init)