Browse Source

Allow awaiting `JsPromise` from Rust code (#3011)

* Allow awaiting `JsPromise` from Rust code

* Fix docs

* Relink to docs.rs
pull/3047/head
José Julián Espina 1 year ago committed by GitHub
parent
commit
1a21cc904f
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
  1. 1
      Cargo.lock
  2. 1
      boa_engine/Cargo.toml
  3. 59
      boa_engine/src/builtins/promise/mod.rs
  4. 64
      boa_engine/src/native_function.rs
  5. 212
      boa_engine/src/object/builtins/jspromise.rs
  6. 13
      boa_gc/src/trace.rs
  7. 2
      boa_icu_provider/src/lib.rs

1
Cargo.lock generated

@ -414,6 +414,7 @@ dependencies = [
"dashmap", "dashmap",
"fast-float", "fast-float",
"float-cmp", "float-cmp",
"futures-lite",
"icu_calendar", "icu_calendar",
"icu_casemapping", "icu_casemapping",
"icu_collator", "icu_collator",

1
boa_engine/Cargo.toml

@ -99,6 +99,7 @@ criterion = "0.4.0"
float-cmp = "0.9.0" float-cmp = "0.9.0"
indoc = "2.0.1" indoc = "2.0.1"
textwrap = "0.16.0" textwrap = "0.16.0"
futures-lite = "1.13.0"
[target.x86_64-unknown-linux-gnu.dev-dependencies] [target.x86_64-unknown-linux-gnu.dev-dependencies]
jemallocator = "0.5.0" jemallocator = "0.5.0"

59
boa_engine/src/builtins/promise/mod.rs

@ -2005,22 +2005,10 @@ impl Promise {
// 9. Return unused. // 9. Return unused.
} }
#[derive(Debug, Trace, Finalize)]
struct RejectResolveCaptures {
promise: JsObject,
#[unsafe_ignore_trace]
already_resolved: Rc<Cell<bool>>,
}
// 1. Let alreadyResolved be the Record { [[Value]]: false }. // 1. Let alreadyResolved be the Record { [[Value]]: false }.
let already_resolved = Rc::new(Cell::new(false));
// 5. Set resolve.[[Promise]] to promise. // 5. Set resolve.[[Promise]] to promise.
// 6. Set resolve.[[AlreadyResolved]] to alreadyResolved. // 6. Set resolve.[[AlreadyResolved]] to alreadyResolved.
let resolve_captures = RejectResolveCaptures { let promise = Gc::new(Cell::new(Some(promise.clone())));
already_resolved: already_resolved.clone(),
promise: promise.clone(),
};
// 2. Let stepsResolve be the algorithm steps defined in Promise Resolve Functions. // 2. Let stepsResolve be the algorithm steps defined in Promise Resolve Functions.
// 3. Let lengthResolve be the number of non-optional parameters of the function definition in Promise Resolve Functions. // 3. Let lengthResolve be the number of non-optional parameters of the function definition in Promise Resolve Functions.
@ -2035,18 +2023,11 @@ impl Promise {
// 2. Assert: F has a [[Promise]] internal slot whose value is an Object. // 2. Assert: F has a [[Promise]] internal slot whose value is an Object.
// 3. Let promise be F.[[Promise]]. // 3. Let promise be F.[[Promise]].
// 4. Let alreadyResolved be F.[[AlreadyResolved]]. // 4. Let alreadyResolved be F.[[AlreadyResolved]].
let RejectResolveCaptures {
promise,
already_resolved,
} = captures;
// 5. If alreadyResolved.[[Value]] is true, return undefined. // 5. If alreadyResolved.[[Value]] is true, return undefined.
if already_resolved.get() {
return Ok(JsValue::Undefined);
}
// 6. Set alreadyResolved.[[Value]] to true. // 6. Set alreadyResolved.[[Value]] to true.
already_resolved.set(true); let Some(promise) = captures.take() else {
return Ok(JsValue::undefined())
};
let resolution = args.get_or_undefined(0); let resolution = args.get_or_undefined(0);
@ -2058,7 +2039,7 @@ impl Promise {
.to_opaque(context); .to_opaque(context);
// b. Perform RejectPromise(promise, selfResolutionError). // b. Perform RejectPromise(promise, selfResolutionError).
reject_promise(promise, self_resolution_error.into(), context); reject_promise(&promise, self_resolution_error.into(), context);
// c. Return undefined. // c. Return undefined.
return Ok(JsValue::Undefined); return Ok(JsValue::Undefined);
@ -2067,7 +2048,7 @@ impl Promise {
let Some(then) = resolution.as_object() else { let Some(then) = resolution.as_object() else {
// 8. If Type(resolution) is not Object, then // 8. If Type(resolution) is not Object, then
// a. Perform FulfillPromise(promise, resolution). // a. Perform FulfillPromise(promise, resolution).
fulfill_promise(promise, resolution.clone(), context); fulfill_promise(&promise, resolution.clone(), context);
// b. Return undefined. // b. Return undefined.
return Ok(JsValue::Undefined); return Ok(JsValue::Undefined);
@ -2078,7 +2059,7 @@ impl Promise {
// 10. If then is an abrupt completion, then // 10. If then is an abrupt completion, then
Err(e) => { Err(e) => {
// a. Perform RejectPromise(promise, then.[[Value]]). // a. Perform RejectPromise(promise, then.[[Value]]).
reject_promise(promise, e.to_opaque(context), context); reject_promise(&promise, e.to_opaque(context), context);
// b. Return undefined. // b. Return undefined.
return Ok(JsValue::Undefined); return Ok(JsValue::Undefined);
@ -2090,7 +2071,7 @@ impl Promise {
// 12. If IsCallable(thenAction) is false, then // 12. If IsCallable(thenAction) is false, then
let Some(then_action) = then_action.as_object().cloned().and_then(JsFunction::from_object) else { let Some(then_action) = then_action.as_object().cloned().and_then(JsFunction::from_object) else {
// a. Perform FulfillPromise(promise, resolution). // a. Perform FulfillPromise(promise, resolution).
fulfill_promise(promise, resolution.clone(), context); fulfill_promise(&promise, resolution.clone(), context);
// b. Return undefined. // b. Return undefined.
return Ok(JsValue::Undefined); return Ok(JsValue::Undefined);
@ -2113,7 +2094,7 @@ impl Promise {
// 16. Return undefined. // 16. Return undefined.
Ok(JsValue::Undefined) Ok(JsValue::Undefined)
}, },
resolve_captures, promise.clone(),
), ),
) )
.name("") .name("")
@ -2123,11 +2104,6 @@ impl Promise {
// 10. Set reject.[[Promise]] to promise. // 10. Set reject.[[Promise]] to promise.
// 11. Set reject.[[AlreadyResolved]] to alreadyResolved. // 11. Set reject.[[AlreadyResolved]] to alreadyResolved.
let reject_captures = RejectResolveCaptures {
promise: promise.clone(),
already_resolved,
};
// 7. Let stepsReject be the algorithm steps defined in Promise Reject Functions. // 7. Let stepsReject be the algorithm steps defined in Promise Reject Functions.
// 8. Let lengthReject be the number of non-optional parameters of the function definition in Promise Reject Functions. // 8. Let lengthReject be the number of non-optional parameters of the function definition in Promise Reject Functions.
// 9. Let reject be CreateBuiltinFunction(stepsReject, lengthReject, "", « [[Promise]], [[AlreadyResolved]] »). // 9. Let reject be CreateBuiltinFunction(stepsReject, lengthReject, "", « [[Promise]], [[AlreadyResolved]] »).
@ -2141,26 +2117,19 @@ impl Promise {
// 2. Assert: F has a [[Promise]] internal slot whose value is an Object. // 2. Assert: F has a [[Promise]] internal slot whose value is an Object.
// 3. Let promise be F.[[Promise]]. // 3. Let promise be F.[[Promise]].
// 4. Let alreadyResolved be F.[[AlreadyResolved]]. // 4. Let alreadyResolved be F.[[AlreadyResolved]].
let RejectResolveCaptures {
promise,
already_resolved,
} = captures;
// 5. If alreadyResolved.[[Value]] is true, return undefined. // 5. If alreadyResolved.[[Value]] is true, return undefined.
if already_resolved.get() {
return Ok(JsValue::Undefined);
}
// 6. Set alreadyResolved.[[Value]] to true. // 6. Set alreadyResolved.[[Value]] to true.
already_resolved.set(true); let Some(promise) = captures.take() else {
return Ok(JsValue::undefined());
};
// 7. Perform RejectPromise(promise, reason). // 7. Perform RejectPromise(promise, reason).
reject_promise(promise, args.get_or_undefined(0).clone(), context); reject_promise(&promise, args.get_or_undefined(0).clone(), context);
// 8. Return undefined. // 8. Return undefined.
Ok(JsValue::Undefined) Ok(JsValue::Undefined)
}, },
reject_captures, promise,
), ),
) )
.name("") .name("")

64
boa_engine/src/native_function.rs

@ -3,11 +3,9 @@
//! [`NativeFunction`] is the main type of this module, providing APIs to create native callables //! [`NativeFunction`] is the main type of this module, providing APIs to create native callables
//! from native Rust functions and closures. //! from native Rust functions and closures.
use std::future::Future;
use boa_gc::{custom_trace, Finalize, Gc, Trace}; use boa_gc::{custom_trace, Finalize, Gc, Trace};
use crate::{job::NativeJob, object::JsPromise, Context, JsResult, JsValue}; use crate::{object::JsPromise, Context, JsResult, JsValue};
/// The required signature for all native built-in function pointers. /// The required signature for all native built-in function pointers.
/// ///
@ -115,14 +113,18 @@ impl NativeFunction {
} }
} }
/// Creates a `NativeFunction` from a function returning a [`Future`]. /// Creates a `NativeFunction` from a function returning a [`Future`]-like.
/// ///
/// The returned `NativeFunction` will return an ECMAScript `Promise` that will be fulfilled /// The returned `NativeFunction` will return an ECMAScript `Promise` that will be fulfilled
/// or rejected when the returned [`Future`] completes. /// or rejected when the returned [`Future`] completes.
/// ///
/// If you only need to convert a [`Future`]-like into a [`JsPromise`], see
/// [`JsPromise::from_future`].
///
/// # Caveats /// # Caveats
/// ///
/// Consider the next snippet: /// Certain async functions need to be desugared for them to be `'static'`. For example, the
/// following won't compile:
/// ///
/// ```compile_fail /// ```compile_fail
/// # use boa_engine::{ /// # use boa_engine::{
@ -144,14 +146,29 @@ impl NativeFunction {
/// NativeFunction::from_async_fn(test); /// NativeFunction::from_async_fn(test);
/// ``` /// ```
/// ///
/// Seems like a perfectly fine code, right? `args` is not used after the await point, which /// Even though `args` is only used before the first await point, Rust's async functions are
/// in theory should make the whole future `'static` ... in theory ... /// fully lazy, which makes `test` equivalent to something like:
///
/// ```
/// # use std::future::Future;
/// # use boa_engine::{JsValue, Context, JsResult};
/// fn test<'a>(
/// _this: &JsValue,
/// args: &'a [JsValue],
/// _context: &mut Context<'_>,
/// ) -> impl Future<Output = JsResult<JsValue>> + 'a {
/// async move {
/// let arg = args.get(0).cloned();
/// std::future::ready(()).await;
/// drop(arg);
/// Ok(JsValue::null())
/// }
/// }
/// ```
/// ///
/// This code unfortunately fails to compile at the moment. This is because `rustc` currently /// Note that `args` is used inside the `async move`, making the whole future not `'static`.
/// cannot determine that `args` can be dropped before the await point, which would trivially
/// make the future `'static`. Track [this issue] for more information.
/// ///
/// In the meantime, a manual desugaring of the async function does the trick: /// In those cases, you can manually restrict the lifetime of the arguments:
/// ///
/// ``` /// ```
/// # use std::future::Future; /// # use std::future::Future;
@ -175,29 +192,18 @@ impl NativeFunction {
/// } /// }
/// NativeFunction::from_async_fn(test); /// NativeFunction::from_async_fn(test);
/// ``` /// ```
/// [this issue]: https://github.com/rust-lang/rust/issues/69663 ///
/// And this should always return a `'static` future.
///
/// [`Future`]: std::future::Future
pub fn from_async_fn<Fut>(f: fn(&JsValue, &[JsValue], &mut Context<'_>) -> Fut) -> Self pub fn from_async_fn<Fut>(f: fn(&JsValue, &[JsValue], &mut Context<'_>) -> Fut) -> Self
where where
Fut: Future<Output = JsResult<JsValue>> + 'static, Fut: std::future::IntoFuture<Output = JsResult<JsValue>> + 'static,
{ {
Self::from_copy_closure(move |this, args, context| { Self::from_copy_closure(move |this, args, context| {
let (promise, resolvers) = JsPromise::new_pending(context);
let future = f(this, args, context); let future = f(this, args, context);
let future = async move {
let result = future.await; Ok(JsPromise::from_future(future, context).into())
NativeJob::new(move |ctx| match result {
Ok(v) => resolvers.resolve.call(&JsValue::undefined(), &[v], ctx),
Err(e) => {
let e = e.to_opaque(ctx);
resolvers.reject.call(&JsValue::undefined(), &[e], ctx)
}
})
};
context
.job_queue()
.enqueue_future_job(Box::pin(future), context);
Ok(promise.into())
}) })
} }

212
boa_engine/src/object/builtins/jspromise.rs

@ -1,5 +1,7 @@
//! A Rust API wrapper for Boa's promise Builtin ECMAScript Object //! A Rust API wrapper for Boa's promise Builtin ECMAScript Object
use std::{future::Future, pin::Pin, task};
use super::{JsArray, JsFunction}; use super::{JsArray, JsFunction};
use crate::{ use crate::{
builtins::{ builtins::{
@ -7,11 +9,12 @@ use crate::{
Promise, Promise,
}, },
context::intrinsics::StandardConstructors, context::intrinsics::StandardConstructors,
object::{JsObject, JsObjectType, ObjectData}, job::NativeJob,
object::{FunctionObjectBuilder, JsObject, JsObjectType, ObjectData},
value::TryFromJs, value::TryFromJs,
Context, JsError, JsNativeError, JsResult, JsValue, Context, JsArgs, JsError, JsNativeError, JsResult, JsValue, NativeFunction,
}; };
use boa_gc::{Finalize, Trace}; use boa_gc::{Finalize, Gc, GcRefCell, Trace};
/// An ECMAScript [promise] object. /// An ECMAScript [promise] object.
/// ///
@ -247,6 +250,61 @@ impl JsPromise {
Ok(Self { inner: object }) Ok(Self { inner: object })
} }
/// Creates a new `JsPromise` from a [`Future`]-like.
///
/// If you want to convert a Rust async function into an ECMAScript async function, see
/// [`NativeFunction::from_async_fn`][async_fn].
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use boa_engine::{
/// # object::builtins::JsPromise,
/// # builtins::promise::PromiseState,
/// # Context, JsResult, JsValue
/// # };
/// # fn main() -> Result<(), Box<dyn Error>> {
/// async fn f() -> JsResult<JsValue> {
/// Ok(JsValue::null())
/// }
/// let context = &mut Context::default();
///
/// let promise = JsPromise::from_future(f(), context);
///
/// context.run_jobs();
///
/// assert_eq!(promise.state()?, PromiseState::Fulfilled(JsValue::null()));
/// # Ok(())
/// # }
/// ```
///
/// [async_fn]: crate::native_function::NativeFunction::from_async_fn
pub fn from_future<Fut>(future: Fut, context: &mut Context<'_>) -> Self
where
Fut: std::future::IntoFuture<Output = JsResult<JsValue>> + 'static,
{
let (promise, resolvers) = Self::new_pending(context);
let future = async move {
let result = future.await;
NativeJob::new(move |context| match result {
Ok(v) => resolvers.resolve.call(&JsValue::undefined(), &[v], context),
Err(e) => {
let e = e.to_opaque(context);
resolvers.reject.call(&JsValue::undefined(), &[e], context)
}
})
};
context
.job_queue()
.enqueue_future_job(Box::pin(future), context);
promise
}
/// Resolves a `JsValue` into a `JsPromise`. /// Resolves a `JsValue` into a `JsPromise`.
/// ///
/// Equivalent to the [`Promise.resolve()`] static method. /// Equivalent to the [`Promise.resolve()`] static method.
@ -833,6 +891,114 @@ impl JsPromise {
Self::from_object(value.clone()) Self::from_object(value.clone())
} }
/// Creates a `JsFuture` from this `JsPromise`.
///
/// The returned `JsFuture` implements [`Future`], which means it can be `await`ed within Rust's
/// async contexts (async functions and async blocks).
///
/// # Examples
///
/// ```
/// # use std::error::Error;
/// # use boa_engine::{
/// # builtins::promise::PromiseState,
/// # object::builtins::JsPromise,
/// # Context, JsValue, JsError
/// # };
/// # use futures_lite::future;
/// # fn main() -> Result<(), Box<dyn Error>> {
/// let context = &mut Context::default();
///
/// let (promise, resolvers) = JsPromise::new_pending(context);
/// let promise_future = promise.into_js_future(context)?;
///
/// let future1 = async move {
/// promise_future.await
/// };
///
/// let future2 = async move {
/// resolvers.resolve.call(&JsValue::undefined(), &[10.into()], context)?;
/// context.run_jobs();
/// Ok::<(), JsError>(())
/// };
///
/// let (result1, result2) = future::block_on(future::zip(future1, future2));
///
/// assert_eq!(result1, Ok(JsValue::from(10)));
/// assert_eq!(result2, Ok(()));
///
/// # Ok(())
/// # }
/// ```
pub fn into_js_future(self, context: &mut Context<'_>) -> JsResult<JsFuture> {
// Mostly based from:
// https://docs.rs/wasm-bindgen-futures/0.4.37/src/wasm_bindgen_futures/lib.rs.html#109-168
fn finish(state: &GcRefCell<Inner>, val: JsResult<JsValue>) {
let task = {
let mut state = state.borrow_mut();
// The engine ensures both `resolve` and `reject` are called only once,
// and only one of them.
debug_assert!(state.result.is_none());
// Store the received value into the state shared by the resolving functions
// and the `JsFuture` itself. This will be accessed when the executor polls
// the `JsFuture` again.
state.result = Some(val);
state.task.take()
};
// `task` could be `None` if the `JsPromise` was already fulfilled before polling
// the `JsFuture`.
if let Some(task) = task {
task.wake();
}
}
let state = Gc::new(GcRefCell::new(Inner {
result: None,
task: None,
}));
let resolve = {
let state = state.clone();
FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_, args, state, _| {
finish(state, Ok(args.get_or_undefined(0).clone()));
Ok(JsValue::undefined())
},
state,
),
)
.build()
};
let reject = {
let state = state.clone();
FunctionObjectBuilder::new(
context,
NativeFunction::from_copy_closure_with_captures(
move |_, args, state, _| {
let err = JsError::from_opaque(args.get_or_undefined(0).clone());
finish(state, Err(err));
Ok(JsValue::undefined())
},
state,
),
)
.build()
};
drop(self.then(Some(resolve), Some(reject), context)?);
Ok(JsFuture { inner: state })
}
} }
impl From<JsPromise> for JsObject { impl From<JsPromise> for JsObject {
@ -870,3 +1036,43 @@ impl TryFromJs for JsPromise {
} }
} }
} }
/// A Rust's `Future` that becomes ready when a `JsPromise` fulfills.
///
/// This type allows `await`ing `JsPromise`s inside Rust's async contexts, which makes interfacing
/// between promises and futures a bit easier.
///
/// The only way to construct an instance of `JsFuture` is by calling [`JsPromise::into_js_future`].
pub struct JsFuture {
inner: Gc<GcRefCell<Inner>>,
}
impl std::fmt::Debug for JsFuture {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("JsFuture").finish_non_exhaustive()
}
}
#[derive(Trace, Finalize)]
struct Inner {
result: Option<JsResult<JsValue>>,
#[unsafe_ignore_trace]
task: Option<task::Waker>,
}
// Taken from:
// https://docs.rs/wasm-bindgen-futures/0.4.37/src/wasm_bindgen_futures/lib.rs.html#171-187
impl Future for JsFuture {
type Output = JsResult<JsValue>;
fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll<Self::Output> {
let mut inner = self.inner.borrow_mut();
if let Some(result) = inner.result.take() {
return task::Poll::Ready(result);
}
inner.task = Some(cx.waker().clone());
task::Poll::Pending
}
}

13
boa_gc/src/trace.rs

@ -1,5 +1,6 @@
use std::{ use std::{
borrow::{Cow, ToOwned}, borrow::{Cow, ToOwned},
cell::Cell,
collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, LinkedList, VecDeque}, collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, LinkedList, VecDeque},
hash::{BuildHasher, Hash}, hash::{BuildHasher, Hash},
marker::PhantomData, marker::PhantomData,
@ -432,3 +433,15 @@ where
} }
}); });
} }
impl<T: Trace> Finalize for Cell<Option<T>> {}
// SAFETY: Taking and setting is done in a single action, and recursive traces should find a `None`
// value instead of the original `T`, making this safe.
unsafe impl<T: Trace> Trace for Cell<Option<T>> {
custom_trace!(this, {
if let Some(v) = this.take() {
mark(&v);
this.set(Some(v));
}
});
}

2
boa_icu_provider/src/lib.rs

@ -1,7 +1,7 @@
//! Boa's **`boa_icu_provider`** exports the default data provider used by its `Intl` implementation. //! Boa's **`boa_icu_provider`** exports the default data provider used by its `Intl` implementation.
//! //!
//! # Crate Overview //! # Crate Overview
//! This crate exports the function [`buffer`], which contains an extensive dataset of locale data to //! This crate exports the function `buffer`, which contains an extensive dataset of locale data to
//! enable `Intl` functionality in the engine. The set of locales included is precisely the ["modern"] //! enable `Intl` functionality in the engine. The set of locales included is precisely the ["modern"]
//! subset of locales in the [Unicode Common Locale Data Repository][cldr]. //! subset of locales in the [Unicode Common Locale Data Repository][cldr].
//! //!

Loading…
Cancel
Save