feat(napi): provide PromiseRaw for non-await scenario (#2168)

This commit is contained in:
LongYinan 2024-07-06 19:09:16 +08:00 committed by GitHub
parent a4cd94ea30
commit bc9e931a4e
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
17 changed files with 367 additions and 31 deletions

View File

@ -1,3 +1,4 @@
use bindgen_prelude::PromiseRaw;
use napi::threadsafe_function::*;
use napi::*;
@ -22,7 +23,7 @@ impl Task for BufferLength {
}
#[js_function(1)]
fn bench_async_task(ctx: CallContext) -> Result<JsObject> {
fn bench_async_task(ctx: CallContext) -> Result<PromiseRaw<JsNumber>> {
let n = ctx.get::<JsBuffer>(0)?;
let task = BufferLength(n.into_ref()?);
let async_promise = ctx.env.spawn(task)?;

View File

@ -213,6 +213,7 @@ static KNOWN_TYPES: Lazy<HashMap<&'static str, (&'static str, bool, bool)>> = La
("Either26", ("{} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {} | {}", false, true)),
("external", ("object", false, false)),
("Promise", ("Promise<{}>", false, false)),
("PromiseRaw", ("PromiseRaw<{}>", false, false)),
("AbortSignal", ("AbortSignal", false, false)),
("JsGlobal", ("typeof global", false, false)),
("External", ("ExternalObject<{}>", false, false)),

View File

@ -1,14 +1,13 @@
use std::ffi::CString;
use std::marker::PhantomData;
use std::mem;
use std::os::raw::c_void;
use std::ptr;
use std::rc::Rc;
use std::sync::atomic::{AtomicU8, Ordering};
use crate::{
bindgen_runtime::ToNapiValue, check_status, js_values::NapiValue, sys, Env, JsError, JsObject,
Result, Task,
};
use crate::bindgen_runtime::PromiseRaw;
use crate::{bindgen_runtime::ToNapiValue, check_status, sys, Env, JsError, Result, Task};
struct AsyncWork<T: Task> {
inner_task: T,
@ -18,7 +17,7 @@ struct AsyncWork<T: Task> {
status: Rc<AtomicU8>,
}
pub struct AsyncWorkPromise {
pub struct AsyncWorkPromise<T> {
pub(crate) napi_async_work: sys::napi_async_work,
raw_promise: sys::napi_value,
pub(crate) deferred: sys::napi_deferred,
@ -28,14 +27,15 @@ pub struct AsyncWorkPromise {
/// 1: completed
/// 2: canceled
pub(crate) status: Rc<AtomicU8>,
_phantom: PhantomData<T>,
}
impl AsyncWorkPromise {
pub fn promise_object(&self) -> JsObject {
unsafe { JsObject::from_raw_unchecked(self.env, self.raw_promise) }
impl<T> AsyncWorkPromise<T> {
pub fn promise_object(&self) -> PromiseRaw<T> {
PromiseRaw::new(self.env, self.raw_promise)
}
pub fn cancel(&self) -> Result<()> {
pub fn cancel(&mut self) -> Result<()> {
// must be happened in the main thread, relaxed is enough
self.status.store(2, Ordering::Relaxed);
check_status!(unsafe { sys::napi_cancel_async_work(self.env, self.napi_async_work) })
@ -46,7 +46,7 @@ pub fn run<T: Task>(
env: sys::napi_env,
task: T,
abort_status: Option<Rc<AtomicU8>>,
) -> Result<AsyncWorkPromise> {
) -> Result<AsyncWorkPromise<T::JsValue>> {
let mut raw_resource = ptr::null_mut();
check_status!(unsafe { sys::napi_create_object(env, &mut raw_resource) })?;
let mut raw_promise = ptr::null_mut();
@ -85,6 +85,7 @@ pub fn run<T: Task>(
deferred,
env,
status: task_status,
_phantom: PhantomData,
})
}

View File

@ -1,5 +1,6 @@
use std::ffi::CStr;
use std::ffi::{CStr, CString};
use std::future;
use std::marker::PhantomData;
use std::pin::Pin;
use std::ptr;
use std::sync::{
@ -12,8 +13,26 @@ use tokio::sync::oneshot::{channel, Receiver, Sender};
use crate::{check_status, sys, Error, JsUnknown, NapiValue, Result, Status};
use super::{FromNapiValue, TypeName, ValidateNapiValue};
use super::{FromNapiValue, ToNapiValue, TypeName, ValidateNapiValue};
/// The JavaScript Promise object representation
///
/// This `Promise<T>` can be awaited in the Rust
/// THis `Promise<T>` can also be passed from `#[napi]` fn
///
/// example:
///
/// ```no_run
/// #[napi]
/// pub fn await_promise_in_rust(promise: Promise<u32>) {
/// let value = promise.await.unwrap();
///
/// println!("{value}");
/// }
/// ```
///
/// But this `Promise<T>` can not be pass back to `JavaScript`.
/// If you want to use raw JavaScript `Promise` API, you can use the [`PromiseRaw`](./PromiseRaw) instead.
pub struct Promise<T: FromNapiValue> {
value: Pin<Box<Receiver<*mut Result<T>>>>,
aborted: Arc<AtomicBool>,
@ -256,3 +275,258 @@ unsafe extern "C" fn catch_callback<T: FromNapiValue>(
})))));
this
}
pub struct PromiseRaw<T> {
pub(crate) inner: sys::napi_value,
env: sys::napi_env,
_phantom: PhantomData<T>,
}
impl<T> PromiseRaw<T> {
pub(crate) fn new(env: sys::napi_env, inner: sys::napi_value) -> Self {
Self {
inner,
env,
_phantom: PhantomData,
}
}
}
impl<T: FromNapiValue> PromiseRaw<T> {
/// Promise.then method
pub fn then<Callback, U>(&mut self, cb: Callback) -> Result<PromiseRaw<U>>
where
U: ToNapiValue,
Callback: FnOnce(T) -> Result<U>,
{
let mut then_fn = ptr::null_mut();
let then_c_string = unsafe { CStr::from_bytes_with_nul_unchecked(b"then\0") };
check_status!(unsafe {
sys::napi_get_named_property(self.env, self.inner, then_c_string.as_ptr(), &mut then_fn)
})?;
let mut then_callback = ptr::null_mut();
let rust_cb = Box::into_raw(Box::new(cb));
check_status!(
unsafe {
sys::napi_create_function(
self.env,
then_c_string.as_ptr(),
4,
Some(raw_promise_then_callback::<T, U, Callback>),
rust_cb.cast(),
&mut then_callback,
)
},
"Create then function for PromiseRaw failed"
)?;
let mut new_promise = ptr::null_mut();
check_status!(
unsafe {
sys::napi_call_function(
self.env,
self.inner,
then_fn,
1,
[then_callback].as_ptr(),
&mut new_promise,
)
},
"Call then callback on PromiseRaw failed"
)?;
Ok(PromiseRaw::<U> {
env: self.env,
inner: new_promise,
_phantom: PhantomData,
})
}
/// Promise.catch method
pub fn catch<E, U, Callback>(&mut self, cb: Callback) -> Result<PromiseRaw<U>>
where
E: FromNapiValue,
U: ToNapiValue,
Callback: FnOnce(E) -> Result<U>,
{
let mut catch_fn = ptr::null_mut();
check_status!(unsafe {
sys::napi_get_named_property(
self.env,
self.inner,
"catch\0".as_ptr().cast(),
&mut catch_fn,
)
})?;
let mut catch_callback = ptr::null_mut();
let rust_cb = Box::into_raw(Box::new(cb));
check_status!(unsafe {
sys::napi_create_function(
self.env,
"catch\0".as_ptr().cast(),
5,
Some(raw_promise_catch_callback::<E, U, Callback>),
rust_cb.cast(),
&mut catch_callback,
)
})?;
let mut new_promise = ptr::null_mut();
check_status!(unsafe {
sys::napi_call_function(
self.env,
self.inner,
catch_fn,
1,
[catch_callback].as_mut_ptr().cast(),
&mut new_promise,
)
})?;
Ok(PromiseRaw::<U> {
env: self.env,
inner: new_promise,
_phantom: PhantomData,
})
}
/// Convert `PromiseRaw<T>` to `Promise<T>`
///
/// So you can await the Promise in Rust
pub fn into_sendable_promise(self) -> Result<Promise<T>> {
unsafe { Promise::from_napi_value(self.env, self.inner) }
}
}
impl<T: FromNapiValue> TypeName for PromiseRaw<T> {
fn type_name() -> &'static str {
"Promise"
}
fn value_type() -> crate::ValueType {
crate::ValueType::Object
}
}
impl<T: FromNapiValue> ValidateNapiValue for PromiseRaw<T> {
unsafe fn validate(
env: napi_sys::napi_env,
napi_val: napi_sys::napi_value,
) -> Result<napi_sys::napi_value> {
Promise::<T>::validate(env, napi_val)
}
}
impl<T> FromNapiValue for PromiseRaw<T> {
unsafe fn from_napi_value(env: sys::napi_env, napi_val: sys::napi_value) -> crate::Result<Self> {
Ok(PromiseRaw {
inner: napi_val,
env,
_phantom: PhantomData,
})
}
}
impl<T> ToNapiValue for PromiseRaw<T> {
unsafe fn to_napi_value(_env: napi_sys::napi_env, val: Self) -> Result<napi_sys::napi_value> {
Ok(val.inner)
}
}
unsafe extern "C" fn raw_promise_then_callback<T, U, Cb>(
env: sys::napi_env,
cbinfo: sys::napi_callback_info,
) -> sys::napi_value
where
T: FromNapiValue,
U: ToNapiValue,
Cb: FnOnce(T) -> Result<U>,
{
match handle_then_callback::<T, U, Cb>(env, cbinfo) {
Ok(v) => v,
Err(err) => {
let code = CString::new(err.status.as_ref()).unwrap();
let msg = CString::new(err.reason).unwrap();
unsafe { sys::napi_throw_error(env, code.as_ptr(), msg.as_ptr()) };
ptr::null_mut()
}
}
}
fn handle_then_callback<T, U, Cb>(
env: sys::napi_env,
cbinfo: sys::napi_callback_info,
) -> Result<sys::napi_value>
where
T: FromNapiValue,
U: ToNapiValue,
Cb: FnOnce(T) -> Result<U>,
{
let mut callback_values = [ptr::null_mut()];
let mut rust_cb = ptr::null_mut();
check_status!(
unsafe {
sys::napi_get_cb_info(
env,
cbinfo,
&mut 1,
callback_values.as_mut_ptr(),
ptr::null_mut(),
&mut rust_cb,
)
},
"Get callback info from then callback failed"
)?;
let then_value: T = unsafe { FromNapiValue::from_napi_value(env, callback_values[0]) }?;
let cb: Box<Cb> = unsafe { Box::from_raw(rust_cb.cast()) };
unsafe { U::to_napi_value(env, cb(then_value)?) }
}
unsafe extern "C" fn raw_promise_catch_callback<E, U, Cb>(
env: sys::napi_env,
cbinfo: sys::napi_callback_info,
) -> sys::napi_value
where
E: FromNapiValue,
U: ToNapiValue,
Cb: FnOnce(E) -> Result<U>,
{
match handle_catch_callback::<E, U, Cb>(env, cbinfo) {
Ok(v) => v,
Err(err) => {
let code = CString::new(err.status.as_ref()).unwrap();
let msg = CString::new(err.reason).unwrap();
unsafe { sys::napi_throw_error(env, code.as_ptr(), msg.as_ptr()) };
ptr::null_mut()
}
}
}
fn handle_catch_callback<E, U, Cb>(
env: sys::napi_env,
cbinfo: sys::napi_callback_info,
) -> Result<sys::napi_value>
where
E: FromNapiValue,
U: ToNapiValue,
Cb: FnOnce(E) -> Result<U>,
{
let mut callback_values = [ptr::null_mut(); 1];
let mut rust_cb = ptr::null_mut();
check_status!(
unsafe {
sys::napi_get_cb_info(
env,
cbinfo,
&mut 1,
callback_values.as_mut_ptr(),
ptr::null_mut(),
&mut rust_cb,
)
},
"Get callback info from catch callback failed"
)?;
let catch_value: E = unsafe { FromNapiValue::from_napi_value(env, callback_values[0]) }?;
let cb: Box<Cb> = unsafe { Box::from_raw(rust_cb.cast()) };
unsafe { U::to_napi_value(env, cb(catch_value)?) }
}

View File

@ -149,10 +149,10 @@ impl<T: Task> ToNapiValue for AsyncTask<T> {
abort_controller
.raw_deferred
.store(async_promise.deferred, Ordering::Relaxed);
Ok(async_promise.promise_object().0.value)
Ok(async_promise.promise_object().inner)
} else {
let async_promise = async_work::run(env, val.inner, None)?;
Ok(async_promise.promise_object().0.value)
Ok(async_promise.promise_object().inner)
}
}
}

View File

@ -18,6 +18,8 @@ use serde::Serialize;
use crate::async_cleanup_hook::AsyncCleanupHook;
#[cfg(feature = "napi5")]
use crate::bindgen_runtime::FunctionCallContext;
#[cfg(all(feature = "tokio_rt", feature = "napi4"))]
use crate::bindgen_runtime::PromiseRaw;
#[cfg(feature = "napi4")]
use crate::bindgen_runtime::ToNapiValue;
use crate::bindgen_runtime::{FromNapiValue, Function, JsValuesTupleIntoVec, Unknown};
@ -998,7 +1000,7 @@ impl Env {
}
/// Run [Task](./trait.Task.html) in libuv thread pool, return [AsyncWorkPromise](./struct.AsyncWorkPromise.html)
pub fn spawn<T: 'static + Task>(&self, task: T) -> Result<AsyncWorkPromise> {
pub fn spawn<T: 'static + Task>(&self, task: T) -> Result<AsyncWorkPromise<T::JsValue>> {
async_work::run(self.0, task, None)
}
@ -1102,6 +1104,7 @@ impl Env {
}
#[cfg(all(feature = "tokio_rt", feature = "napi4"))]
#[deprecated(since = "3.0.0", note = "Please use `Env::spawn_future` instead")]
pub fn execute_tokio_future<
T: 'static + Send,
V: 'static + ToNapiValue,
@ -1122,20 +1125,43 @@ impl Env {
}
#[cfg(all(feature = "tokio_rt", feature = "napi4"))]
/// Spawn a future, return a JavaScript Promise which takes the result of the future
pub fn spawn_future<
T: 'static + Send + ToNapiValue,
F: 'static + Send + Future<Output = Result<T>>,
>(
&self,
fut: F,
) -> Result<JsObject> {
) -> Result<PromiseRaw<T>> {
use crate::tokio_runtime;
let promise = tokio_runtime::execute_tokio_future(self.0, fut, |env, val| unsafe {
ToNapiValue::to_napi_value(env, val)
})?;
Ok(unsafe { JsObject::from_raw_unchecked(self.0, promise) })
Ok(PromiseRaw::new(self.0, promise))
}
#[cfg(all(feature = "tokio_rt", feature = "napi4"))]
/// Spawn a future with a callback
/// So you can access the `Env` and resolved value after the future completed
pub fn spawn_future_with_callback<
T: 'static + Send + ToNapiValue,
F: 'static + Send + Future<Output = Result<T>>,
R: 'static + FnOnce(&mut Env, &mut T) -> Result<()>,
>(
&self,
fut: F,
callback: R,
) -> Result<PromiseRaw<T>> {
use crate::tokio_runtime;
let promise = tokio_runtime::execute_tokio_future(self.0, fut, move |env, mut val| unsafe {
callback(&mut Env::from_raw(env), &mut val)?;
ToNapiValue::to_napi_value(env, val)
})?;
Ok(PromiseRaw::new(self.0, promise))
}
/// Creates a deferred promise, which can be resolved or rejected from a background thread.

View File

@ -1,7 +1,8 @@
use std::convert::TryInto;
use napi::{
CallContext, Env, Error, JsBuffer, JsBufferValue, JsNumber, JsObject, Ref, Result, Task,
bindgen_prelude::PromiseRaw, CallContext, Env, Error, JsBuffer, JsBufferValue, JsNumber,
JsObject, Ref, Result, Task,
};
struct ComputeFib {
@ -35,7 +36,7 @@ fn fibonacci_native(n: u32) -> u32 {
}
#[js_function(1)]
fn test_spawn_thread(ctx: CallContext) -> Result<JsObject> {
fn test_spawn_thread(ctx: CallContext) -> Result<PromiseRaw<JsNumber>> {
let n = ctx.get::<JsNumber>(0)?;
let task = ComputeFib::new(n.try_into()?);
let async_promise = ctx.env.spawn(task)?;
@ -67,7 +68,7 @@ impl Task for CountBufferLength {
env.create_uint32(output as _)
}
fn reject(&mut self, env: Env, err: Error) -> Result<Self::JsValue> {
fn reject(&mut self, _env: Env, err: Error) -> Result<Self::JsValue> {
Err(err)
}
@ -78,7 +79,7 @@ impl Task for CountBufferLength {
}
#[js_function(1)]
fn test_spawn_thread_with_ref(ctx: CallContext) -> Result<JsObject> {
fn test_spawn_thread_with_ref(ctx: CallContext) -> Result<PromiseRaw<JsNumber>> {
let n = ctx.get::<JsBuffer>(0)?.into_ref()?;
let task = CountBufferLength::new(n);
let async_work_promise = ctx.env.spawn(task)?;

View File

@ -338,6 +338,8 @@ Generated by [AVA](https://avajs.dev).
export declare function callbackReturnPromiseAndSpawn(jsFunc: (arg0: string) => Promise<string>): Promise<string>
export declare function callCatchOnPromise(input: PromiseRaw<number>): PromiseRaw<string>
export declare function callFunction(cb: () => number): number␊
export declare function callFunctionWithArg(cb: (arg0: number, arg1: number) => number, arg0: number, arg1: number): number␊
@ -346,6 +348,8 @@ Generated by [AVA](https://avajs.dev).
export declare function callLongThreadsafeFunction(tsfn: (err: Error | null, arg: number) => unknown): void␊
export declare function callThenOnPromise(input: PromiseRaw<number>): PromiseRaw<string>
export declare function callThreadsafeFunction(tsfn: (err: Error | null, arg: number) => unknown): void␊
export declare function captureErrorInCallback(cb1: () => void, cb2: (arg0: Error) => void): void␊

View File

@ -183,6 +183,8 @@ import {
panicInAsync,
CustomStruct,
uInit8ArrayFromString,
callThenOnPromise,
callCatchOnPromise,
} from '../index.cjs'
import { test } from './test.framework.js'
@ -491,6 +493,13 @@ Napi4Test('callback function return Promise and spawn', async (t) => {
t.is(finalReturn, 'Hello world 😼')
})
test('promise', async (t) => {
const res = await callThenOnPromise(Promise.resolve(1))
t.is(res, '1')
const cat = await callCatchOnPromise(Promise.reject('cat'))
t.is(cat, 'cat')
})
test('object', (t) => {
t.deepEqual(listObjKeys({ name: 'John Doe', age: 20 }), ['name', 'age'])
t.deepEqual(createObj(), { test: 1 })

View File

@ -434,10 +434,12 @@ module.exports.call1 = nativeBinding.call1
module.exports.call2 = nativeBinding.call2
module.exports.callbackReturnPromise = nativeBinding.callbackReturnPromise
module.exports.callbackReturnPromiseAndSpawn = nativeBinding.callbackReturnPromiseAndSpawn
module.exports.callCatchOnPromise = nativeBinding.callCatchOnPromise
module.exports.callFunction = nativeBinding.callFunction
module.exports.callFunctionWithArg = nativeBinding.callFunctionWithArg
module.exports.callFunctionWithArgAndCtx = nativeBinding.callFunctionWithArgAndCtx
module.exports.callLongThreadsafeFunction = nativeBinding.callLongThreadsafeFunction
module.exports.callThenOnPromise = nativeBinding.callThenOnPromise
module.exports.callThreadsafeFunction = nativeBinding.callThreadsafeFunction
module.exports.captureErrorInCallback = nativeBinding.captureErrorInCallback
module.exports.chronoDateAdd1Minute = nativeBinding.chronoDateAdd1Minute

View File

@ -328,6 +328,8 @@ export declare function callbackReturnPromise<T>(functionInput: () => T | Promis
export declare function callbackReturnPromiseAndSpawn(jsFunc: (arg0: string) => Promise<string>): Promise<string>
export declare function callCatchOnPromise(input: PromiseRaw<number>): PromiseRaw<string>
export declare function callFunction(cb: () => number): number
export declare function callFunctionWithArg(cb: (arg0: number, arg1: number) => number, arg0: number, arg1: number): number
@ -336,6 +338,8 @@ export declare function callFunctionWithArgAndCtx(ctx: Animal, cb: (arg: string)
export declare function callLongThreadsafeFunction(tsfn: (err: Error | null, arg: number) => unknown): void
export declare function callThenOnPromise(input: PromiseRaw<number>): PromiseRaw<string>
export declare function callThreadsafeFunction(tsfn: (err: Error | null, arg: number) => unknown): void
export declare function captureErrorInCallback(cb1: () => void, cb2: (arg0: Error) => void): void

View File

@ -78,7 +78,7 @@ fn callback_return_promise<T: Fn() -> Result<JsUnknown>>(
pub fn callback_return_promise_and_spawn<F: Fn(String) -> Result<Promise<String>>>(
env: Env,
js_func: F,
) -> napi::Result<Object> {
) -> napi::Result<PromiseRaw<String>> {
let promise = js_func("Hello".to_owned())?;
env.spawn_future(async move {
let resolved = promise.await?;

View File

@ -1,9 +1,9 @@
#![allow(deprecated)]
use napi::{
bindgen_prelude::{ClassInstance, Function, FunctionRef},
bindgen_prelude::{ClassInstance, Function, FunctionRef, PromiseRaw},
threadsafe_function::ThreadsafeFunctionCallMode,
Env, Error, JsObject, Result, Status,
Env, Error, Result, Status,
};
use crate::class::Animal;
@ -48,9 +48,9 @@ pub fn call_function_with_arg(cb: Function<(u32, u32), u32>, arg0: u32, arg1: u3
}
#[napi(ts_return_type = "Promise<void>")]
pub fn create_reference_on_function(env: Env, cb: Function<(), ()>) -> Result<JsObject> {
pub fn create_reference_on_function(env: Env, cb: Function<(), ()>) -> Result<PromiseRaw<()>> {
let reference = cb.create_ref()?;
env.execute_tokio_future(
env.spawn_future_with_callback(
async {
tokio::time::sleep(std::time::Duration::from_millis(100)).await;
Ok(())

View File

@ -5,3 +5,13 @@ pub async fn async_plus_100(p: Promise<u32>) -> Result<u32> {
let v = p.await?;
Ok(v + 100)
}
#[napi]
pub fn call_then_on_promise(mut input: PromiseRaw<u32>) -> Result<PromiseRaw<String>> {
input.then(|v| Ok(format!("{}", v)))
}
#[napi]
pub fn call_catch_on_promise(mut input: PromiseRaw<u32>) -> Result<PromiseRaw<String>> {
input.catch(|e: String| Ok(format!("{}", e)))
}

View File

@ -98,7 +98,10 @@ pub fn tsfn_call_with_callback(tsfn: ThreadsafeFunction<(), String>) -> napi::Re
}
#[napi(ts_return_type = "Promise<void>")]
pub fn tsfn_async_call(env: Env, func: Function<(u32, u32, u32), String>) -> napi::Result<Object> {
pub fn tsfn_async_call(
env: Env,
func: Function<(u32, u32, u32), String>,
) -> napi::Result<PromiseRaw<()>> {
let tsfn = func.build_threadsafe_function().build()?;
env.spawn_future(async move {

View File

@ -42,7 +42,7 @@ pub struct Room {
}
#[napi]
pub fn test_async(env: Env) -> napi::Result<napi::JsObject> {
pub fn test_async(env: Env) -> napi::Result<napi::bindgen_prelude::PromiseRaw<String>> {
let data = serde_json::json!({
"findFirstBooking": {
"id": "ckovh15xa104945sj64rdk8oas",
@ -64,11 +64,11 @@ pub fn test_async(env: Env) -> napi::Result<napi::JsObject> {
"room": { "id": "ckovh15xa104955sj6r2tqaw1c", "name": "38683b87f2664" }
}
});
env.execute_tokio_future(
env.spawn_future_with_callback(
async move { Ok(serde_json::to_string(&data).unwrap()) },
|env, res| {
env.adjust_external_memory(res.len() as i64)?;
env.create_string_from_std(res)
Ok(())
},
)
}