diff --git a/Cargo.lock b/Cargo.lock index a2c7bc39d5..121aa4eca4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -414,6 +414,7 @@ dependencies = [ "dashmap", "fast-float", "float-cmp", + "futures-lite", "icu_calendar", "icu_casemapping", "icu_collator", diff --git a/boa_engine/Cargo.toml b/boa_engine/Cargo.toml index eb40366365..55c7701d54 100644 --- a/boa_engine/Cargo.toml +++ b/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" diff --git a/boa_engine/src/builtins/promise/mod.rs b/boa_engine/src/builtins/promise/mod.rs index 136f1346a3..f0882847d1 100644 --- a/boa_engine/src/builtins/promise/mod.rs +++ b/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>, - } - // 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("") diff --git a/boa_engine/src/native_function.rs b/boa_engine/src/native_function.rs index d1d6448285..886787ed43 100644 --- a/boa_engine/src/native_function.rs +++ b/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: + /// + /// ``` + /// # use std::future::Future; + /// # use boa_engine::{JsValue, Context, JsResult}; + /// fn test<'a>( + /// _this: &JsValue, + /// args: &'a [JsValue], + /// _context: &mut Context<'_>, + /// ) -> impl Future> + '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 - /// cannot determine that `args` can be dropped before the await point, which would trivially - /// make the future `'static`. Track [this issue] for more information. + /// Note that `args` is used inside the `async move`, making the whole future not `'static`. /// - /// 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; @@ -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(f: fn(&JsValue, &[JsValue], &mut Context<'_>) -> Fut) -> Self where - Fut: Future> + 'static, + Fut: std::future::IntoFuture> + '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()) }) } diff --git a/boa_engine/src/object/builtins/jspromise.rs b/boa_engine/src/object/builtins/jspromise.rs index 85f292db9e..e596eabd64 100644 --- a/boa_engine/src/object/builtins/jspromise.rs +++ b/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> { + /// async fn f() -> JsResult { + /// 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(future: Fut, context: &mut Context<'_>) -> Self + where + Fut: std::future::IntoFuture> + '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> { + /// 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 { + // 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, val: JsResult) { + 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 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>, +} + +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>, + #[unsafe_ignore_trace] + task: Option, +} + +// 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; + + fn poll(self: Pin<&mut Self>, cx: &mut task::Context<'_>) -> task::Poll { + 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 + } +} diff --git a/boa_gc/src/trace.rs b/boa_gc/src/trace.rs index 3692fb7e94..37a05cee13 100644 --- a/boa_gc/src/trace.rs +++ b/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 Finalize for Cell> {} +// 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 Trace for Cell> { + custom_trace!(this, { + if let Some(v) = this.take() { + mark(&v); + this.set(Some(v)); + } + }); +} diff --git a/boa_icu_provider/src/lib.rs b/boa_icu_provider/src/lib.rs index ee9df26c88..f14c3f858a 100644 --- a/boa_icu_provider/src/lib.rs +++ b/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]. //!