-
Notifications
You must be signed in to change notification settings - Fork 30k
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
WIP: N-API: Implement Promise #13717
Conversation
Personally I'd much prefer the // status checks omitted
napi_value promise, resolution;
napi_create_promise(env, &promise);
napi_create_number(env, 42, &resolution);
napi_resolve_promise(env, &promise, &resolution); |
doc/api/n-api.md
Outdated
|
||
### napi_create_promise | ||
<!-- YAML | ||
added: v8.1.2 |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use added: REPLACEME
here (and below)?
doc/api/n-api.md
Outdated
@@ -2993,6 +2994,74 @@ support it: | |||
* If the function is not available, provide an alternate implementation | |||
that does not use the function. | |||
|
|||
## Promises | |||
|
|||
Promises facilitate the construction of chains of asynchronous actions each of |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think there should be a comma after actions
doc/api/n-api.md
Outdated
|
||
Returns `napi_ok` if the API succeeded. | ||
|
||
This API checks whether a passed-in value is a promise and returns the result |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it might be worth mentioning that this checks whether it is a built-in native Promise, not just any promise object that implements the Promises/A+ interface
src/node_api.cc
Outdated
bool is_resolve) { | ||
napi_status status; | ||
size_t argc = 1; | ||
void *data; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you use left-aligning style (i.e. void* data
)?
src/node_api.cc
Outdated
|
||
// Retrieve the V8 resolver corresponding to this promise. | ||
v8_resolver = | ||
((v8::Persistent<v8::Promise::Resolver> *)data)->Get(env->isolate); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
static_cast
src/node_api.cc
Outdated
// conclude the promise. | ||
if (is_resolve) { | ||
v8_resolver->Resolve(env->isolate->GetCurrentContext(), | ||
v8impl::V8LocalValueFromJsValue(final_value)); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Can you try to align the arguments vertically? (If that brings the line over 80 chars: wrap after the (
and indent both arguments with 4 spaces)
src/node_api.cc
Outdated
// Return everything | ||
(*result) = promise; | ||
(*resolve_callback) = resolve; | ||
(*reject_callback) = reject; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Why the parentheses?
assert.ok(false, | ||
'Promise was resolved when it should have been rejected: ' + | ||
result + ': ' + JSON.stringify(result, null, 4)); | ||
}, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If you want to be lazy, you can just do common.mustNotCall()
instead of the assert.ok(false)
calllback
'Promise was resolved when it should have been rejected: ' + | ||
result + ': ' + JSON.stringify(result, null, 4)); | ||
}, | ||
function(error) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The reject callback should probably be wrapped in common.mustCall(…)
@TimothyGu I also started with that concept at first, but I realized that if I want to provide that interface, I have to tack the Another drawback is that, if you get a Promise that was created in JS, you can't resolve because it doesn't have any Now, the fact that you cannot resolve a promise you receive from JS is also true with the current API, but with the current API you do not have a function that sounds like it's able to resolve a promise. |
Just by the way, V8 doesn’t actually make a difference between the |
OTOH, we could go with // We introduce the concept of the deferred in the documentation, and that
// resolving a promise requires the deferred, not the promise.
NAPI_EXTERN napi_status napi_create_deferred(napi_env env,
napi_value* result);
NAPI_EXTERN napi_status napi_get_promise_from_deferred(napi_env env,
napi_value deferred,
napi_value* promise);
NAPI_EXTERN napi_status napi_resolve_deferred(napi_env env,
napi_value deferred,
napi_value conclusion);
NAPI_EXTERN napi_status napi_reject_deferred(napi_env env,
napi_value deferred,
napi_value conclusion); |
@addaleax OK, but is that an implementation detail I can rely on? I also need to see how other engines do this because, whatever we go with needs to work across multiple engines. |
If we go with the deferred, we have considerable leeway in how we construct the deferred, meaning we should have ample elbow room across engines. |
For deferred we could even introduce a new type, like |
@gabrielschulhof is there a fundamental reason we can't resolve a promise created in js in native code ? |
{ "promise", NULL, NULL, NULL, NULL, promise, napi_default, NULL }, | ||
{ "resolve", NULL, NULL, NULL, NULL, resolve, napi_default, NULL }, | ||
{ "reject", NULL, NULL, NULL, NULL, reject, napi_default, NULL }, | ||
}; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The test uses the pattern where the promise is created in the native code and the resolve, reject are called through JS. If this is the most common use case I wonder a helper (or simply returning it from the original call) to create JS object would make any sense.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don't believe the primary use case is to return a promise to JS so that it may be resolved in JS. That can be accomplished without native code.
I believe the primary use case is to return a promise to JS that will be resolved from the native side - in which case you would never construct such an object. In fact, I only construct it here to make testing out the promise easier by allowing one to use JS to resolve/reject the promise.
OTOH, you're right in that you might have to pass the resolve and reject around the native code such that they are always together, resulting in helper structures or a fatter stack. The deferred paradigm helps here too, because then you only have to pass one object around - the deferred.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
... and in V8 at least, the deferred has engine support for implementing napi_resolve_deferred()
and napi_reject_deferred()
in the form of v8::Promise::Resolver::Resolve()
resp. v8::Promise::Resolver::Reject()
.
In ChakraCore I suspect the deferred will take the form of a JS object with two methods.
@mhdawson if we look at the following code addon.resolve_a_promise(new Promise(function(resolve, reject) {
do_something_asynchronous(function(error, data) {
if (error) {
reject(error);
return;
}
resolve(data);
});
})); then, if the native implementation of At the same time, we must not write an API which ends up having to distinguish between promises created by native code and promises created with JS and passed into native code. Fundamentally, only the place where the promise was created should be the place where it can be resolved. In JS this is easy to enforce by the way in which a promise is constructed. On the native side this locality could never really be enforced because one could simply take persistent references of the resolve and reject and pass them wherever. Thus, I believe the deferred paradigm is better. The code which has access to the deferred is able to resolve/reject the promise, whereas the code which has access to only the promise is only able to chain off of it, but is unable to resolve/reject it. |
@gabrielschulhof Thank you for the explanation, and it makes sense to me why a deferred paradigm is preferred. Now about implementation: I'm somewhat unconvinced that it should be a new type rather than |
c53a6f8
to
21ae18c
Compare
@TimothyGu @addaleax @mhdawson I have now pushed the implementation that uses deferred. |
doc/api/n-api.md
Outdated
added: REPLACEME | ||
--> | ||
```C | ||
NAPI_EXTERN napi_status napi_create_promise(napi_env env, |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
napi_create_deferred
(as it is in the heading of the section)?
doc/api/n-api.md
Outdated
`napi_create_promise()` and the deferred object returned from that call must | ||
have been retained in order to be passed to this API. | ||
|
||
### napi_is_promise |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Do we need a corresponding napi_is_deferred
?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There's no corresponding IsResolver()
in V8. I could mark it with some funky property, but that would be duck typing.
NAPI_CALL(env, napi_create_reference(env, deferred, 1, &deferred_ref)); | ||
|
||
return promise; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
There should be a test making sure any attempt to pass a deferred into JS will error out (in fact I'm not sure it will currently).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It's not forbidden to pass a deferred into JS, it's just not very useful.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
From #13717 (comment), it seems like V8 treats a Resolver just like a Promise, which means that if the user passes the deferred (backed by a Resolver) back into JS the separation between deferred and promise would not be enforced, and the problem you outlined in #13717 (comment) would still be there
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@TimothyGu the fact that a v8::Promise::Resolver === v8::Promise
seems like a shortcoming of V8. The only thing I can think of doing on the N-API side to enforce the distinction between a deferred and a promise is somewhat convoluted:
- To cover creation:
- Declare a JS class "Deferred" and give it an internal field. This would currently have to be done once per N-API module, because we don't have proper per-isolate storage. This means that passing a "Deferred" between N-API modules would not work - which is perhaps not a bad thing.
- Store a persistent reference to an instance of
v8::Resolver
in the internal field of an instance ofDeferred
. - Allow the instance of
Deferred
to be passed into JS.
- To cover usage:
- Given a
napi_value
that claims to be aDeferred
, test it withnapi_instanceof()
against theDeferred
constructor. - If it passes, then retrieve the internal field and obtain a local reference to the
Resolver
from the persistent reference.
- Given a
The only alternative I can currently think of is documentation. Put bluntly, "Yes, you can pass a Deferred into JS, yes it may even behave like a promise, but we're telling you not to do that, because it's not guaranteed to behave like a promise if we ever swap out the underlying engine or if V8 decides to introduce a distinction, and we will make no efforts to support that use case given that you can easily retrieve a bona fide JavaScript promise from a Deferred."
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Of course, if we were to use the original interface of
napi_create_promise(env, &promise, &resolve, &reject);
where resolve
and reject
are to be called with napi_call_function()
then this problem would go away, but, like you said, this interface is preferable to that one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I guess, come to think of it, doing things the convoluted way is not that different from going part-ways towards implementing the original &promise, &resolve, &reject
interface, except that resolve
and reject
are rolled into one.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
It does seem like we'd want to prevent the deferred object from being used as promise as I don't think we can ensure that's the case for all js implementations. I think it will just be too easy for an impl to pass back a deferred and then use it as a promise, and if it works we'll have code that breaks across different runtimes.
We use this for wrap/unwrap:
// Search the object's prototype chain for the wrapper with an internal field.
// Usually the wrapper would be the first in the chain, but it is OK for
// other objects to be inserted in the prototype chain.
v8::Local<v8::Object> wrapper = obj;
do {
v8::Local<v8::Value> proto = wrapper->GetPrototype();
RETURN_STATUS_IF_FALSE(
env, !proto.IsEmpty() && proto->IsObject(), napi_invalid_arg);
wrapper = proto.As<v8::Object>();
} while (wrapper->InternalFieldCount() != 1);
v8::Local<v8::Value> unwrappedValue = wrapper->GetInternalField(0);
RETURN_STATUS_IF_FALSE(env, unwrappedValue->IsExternal(), napi_invalid_arg);
*result = unwrappedValue.As<v8::External>()->Value();
So maybe we should generalize the code and use it for wrap and for creating a deferred object where the resolver is hidden in an internal field of the prototype chain ?
We could also strengthen it by adding a type as a secondary field so that we can more strongly validate it is what we expect it is.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, then I'll take the type-enforcing wrapping approach.
NAPI_CALL(env, napi_conclude_deferred(env, deferred, argv[0], resolution)); | ||
|
||
return NULL; | ||
} |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And there should be a test making sure passing a promise into napi_conclude_deferred
is disallowed.
21ae18c
to
422bea7
Compare
I'd like to wait on the outcome of #13872 before proceeding here, because I'd like to factor out how we wrap things once that lands. |
422bea7
to
3e17884
Compare
napi_create_promise()
creates a promise object and returns the resolveand reject callbacks as JS functions which can be called with
napi_call_function()
, passing anapi_value
as the sole parameter toindicate the resolution of the promise resp. the reason for rejection.
napi_is_promise()
is a type check API returningtrue
if the valuepassed in is a promise, and
false
otherwise.Fixes nodejs/abi-stable-node#242
Checklist
make -j4 test
(UNIX), orvcbuild test
(Windows) passesAffected core subsystem(s)
N-API