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",
"fast-float",
"float-cmp",
"futures-lite",
"icu_calendar",
"icu_casemapping",
"icu_collator",

1
boa_engine/Cargo.toml

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

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

@ -2005,22 +2005,10 @@ impl Promise {
// 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 }.
let already_resolved = Rc::new(Cell::new(false));
// 5. Set resolve.[[Promise]] to promise.
// 6. Set resolve.[[AlreadyResolved]] to alreadyResolved.
let resolve_captures = RejectResolveCaptures {
already_resolved: already_resolved.clone(),
promise: promise.clone(),
};
let promise = Gc::new(Cell::new(Some(promise.clone())));
// 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.
@ -2035,18 +2023,11 @@ impl Promise {
// 2. Assert: F has a [[Promise]] internal slot whose value is an Object.
// 3. Let promise be F.[[Promise]].
// 4. Let alreadyResolved be F.[[AlreadyResolved]].
let RejectResolveCaptures {
promise,
already_resolved,
} = captures;
// 5. If alreadyResolved.[[Value]] is true, return undefined.
if already_resolved.get() {
return Ok(JsValue::Undefined);
}
// 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);
@ -2058,7 +2039,7 @@ impl Promise {
.to_opaque(context);
// b. Perform RejectPromise(promise, selfResolutionError).
reject_promise(promise, self_resolution_error.into(), context);
reject_promise(&promise, self_resolution_error.into(), context);
// c. Return undefined.
return Ok(JsValue::Undefined);
@ -2067,7 +2048,7 @@ impl Promise {
let Some(then) = resolution.as_object() else {
// 8. If Type(resolution) is not Object, then
// a. Perform FulfillPromise(promise, resolution).
fulfill_promise(promise, resolution.clone(), context);
fulfill_promise(&promise, resolution.clone(), context);
// b. Return undefined.
return Ok(JsValue::Undefined);
@ -2078,7 +2059,7 @@ impl Promise {
// 10. If then is an abrupt completion, then
Err(e) => {
// 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.
return Ok(JsValue::Undefined);
@ -2090,7 +2071,7 @@ impl Promise {
// 12. If IsCallable(thenAction) is false, then
let Some(then_action) = then_action.as_object().cloned().and_then(JsFunction::from_object) else {
// a. Perform FulfillPromise(promise, resolution).
fulfill_promise(promise, resolution.clone(), context);
fulfill_promise(&promise, resolution.clone(), context);
// b. Return undefined.
return Ok(JsValue::Undefined);
@ -2113,7 +2094,7 @@ impl Promise {
// 16. Return undefined.
Ok(JsValue::Undefined)
},
resolve_captures,
promise.clone(),
),
)
.name("")
@ -2123,11 +2104,6 @@ impl Promise {
// 10. Set reject.[[Promise]] to promise.
// 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.
// 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]] »).
@ -2141,26 +2117,19 @@ impl Promise {
// 2. Assert: F has a [[Promise]] internal slot whose value is an Object.
// 3. Let promise be F.[[Promise]].
// 4. Let alreadyResolved be F.[[AlreadyResolved]].
let RejectResolveCaptures {
promise,
already_resolved,
} = captures;
// 5. If alreadyResolved.[[Value]] is true, return undefined.
if already_resolved.get() {
return Ok(JsValue::Undefined);
}
// 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).
reject_promise(promise, args.get_or_undefined(0).clone(), context);
reject_promise(&promise, args.get_or_undefined(0).clone(), context);
// 8. Return undefined.
Ok(JsValue::Undefined)
},
reject_captures,
promise,
),
)
.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
//! from native Rust functions and closures.
use std::future::Future;
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.
///
@ -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
/// or rejected when the returned [`Future`] completes.
///
/// If you only need to convert a [`Future`]-like into a [`JsPromise`], see
/// [`JsPromise::from_future`].
///
/// # 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
/// # use boa_engine::{
@ -144,14 +146,29 @@ impl NativeFunction {
/// NativeFunction::from_async_fn(test);
/// ```
///
/// Seems like a perfectly fine code, right? `args` is not used after the await point, which
/// in theory should make the whole future `'static` ... in theory ...
/// Even though `args` is only used before the first await point, Rust's async functions are
/// fully lazy, which makes `test` equivalent to something like:
///
/// This code unfortunately fails to compile at the moment. This is because `rustc` currently
/// cannot determine that `args` can be dropped before the await point, which would trivially
/// make the future `'static`. Track [this issue] for more information.
/// ```
/// # 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())
/// }
/// }
/// ```
///
/// In the meantime, a manual desugaring of the async function does the trick:
/// Note that `args` is used inside the `async move`, making the whole future not `'static`.
///
/// In those cases, you can manually restrict the lifetime of the arguments:
///
/// ```
/// # use std::future::Future;
@ -175,29 +192,18 @@ impl NativeFunction {
/// }
/// 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
where
Fut: Future<Output = JsResult<JsValue>> + 'static,
Fut: std::future::IntoFuture<Output = JsResult<JsValue>> + 'static,
{
Self::from_copy_closure(move |this, args, context| {
let (promise, resolvers) = JsPromise::new_pending(context);
let future = f(this, args, context);
let future = async move {
let result = future.await;
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())
Ok(JsPromise::from_future(future, context).into())
})
}

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

@ -1,5 +1,7 @@
//! A Rust API wrapper for Boa's promise Builtin ECMAScript Object
use std::{future::Future, pin::Pin, task};
use super::{JsArray, JsFunction};
use crate::{
builtins::{
@ -7,11 +9,12 @@ use crate::{
Promise,
},
context::intrinsics::StandardConstructors,
object::{JsObject, JsObjectType, ObjectData},
job::NativeJob,
object::{FunctionObjectBuilder, JsObject, JsObjectType, ObjectData},
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.
///
@ -247,6 +250,61 @@ impl JsPromise {
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`.
///
/// Equivalent to the [`Promise.resolve()`] static method.
@ -833,6 +891,114 @@ impl JsPromise {
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 {
@ -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::{
borrow::{Cow, ToOwned},
cell::Cell,
collections::{BTreeMap, BTreeSet, BinaryHeap, HashMap, HashSet, LinkedList, VecDeque},
hash::{BuildHasher, Hash},
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.
//!
//! # 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"]
//! subset of locales in the [Unicode Common Locale Data Repository][cldr].
//!

Loading…
Cancel
Save