Skip to content
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

[Merged by Bors] - API to construct a NativeFunction from a native async function #2542

Closed
wants to merge 13 commits into from
715 changes: 483 additions & 232 deletions Cargo.lock

Large diffs are not rendered by default.

1 change: 1 addition & 0 deletions boa_cli/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ serde_json = "1.0.93"
colored = "2.0.0"
regex = "1.7.1"
phf = { version = "0.11.1", features = ["macros"] }
pollster = "0.3.0"

[features]
default = ["intl"]
Expand Down
7 changes: 6 additions & 1 deletion boa_cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ mod helper;
use boa_ast::StatementList;
use boa_engine::{
context::ContextBuilder,
job::{JobQueue, NativeJob},
job::{FutureJob, JobQueue, NativeJob},
vm::flowgraph::{Direction, Graph},
Context, JsResult, Source,
};
Expand Down Expand Up @@ -386,4 +386,9 @@ impl JobQueue for Jobs {
}
}
}

fn enqueue_future_job(&self, future: FutureJob, _: &mut Context<'_>) {
let job = pollster::block_on(future);
self.0.borrow_mut().push_front(job);
}
}
1 change: 1 addition & 0 deletions boa_engine/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ static_assertions = "1.1.0"
thiserror = "1.0.38"
dashmap = "5.4.0"
num_enum = "0.5.10"
pollster = "0.3.0"

# intl deps
boa_icu_provider = { workspace = true, optional = true }
Expand Down
33 changes: 19 additions & 14 deletions boa_engine/src/builtins/promise/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -338,16 +338,11 @@ impl BuiltInConstructor for Promise {

let promise = JsObject::from_proto_and_data(
promise,
ObjectData::promise(Self {
// 4. Set promise.[[PromiseState]] to pending.
state: PromiseState::Pending,
// 5. Set promise.[[PromiseFulfillReactions]] to a new empty List.
fulfill_reactions: Vec::new(),
// 6. Set promise.[[PromiseRejectReactions]] to a new empty List.
reject_reactions: Vec::new(),
// 7. Set promise.[[PromiseIsHandled]] to false.
handled: false,
}),
// 4. Set promise.[[PromiseState]] to pending.
// 5. Set promise.[[PromiseFulfillReactions]] to a new empty List.
// 6. Set promise.[[PromiseRejectReactions]] to a new empty List.
// 7. Set promise.[[PromiseIsHandled]] to false.
ObjectData::promise(Self::new()),
);

// 8. Let resolvingFunctions be CreateResolvingFunctions(promise).
Expand Down Expand Up @@ -378,12 +373,22 @@ impl BuiltInConstructor for Promise {
}

#[derive(Debug)]
struct ResolvingFunctionsRecord {
resolve: JsFunction,
reject: JsFunction,
pub(crate) struct ResolvingFunctionsRecord {
pub(crate) resolve: JsFunction,
pub(crate) reject: JsFunction,
}

impl Promise {
/// Creates a new, pending `Promise`.
pub(crate) fn new() -> Self {
Promise {
state: PromiseState::Pending,
fulfill_reactions: Vec::default(),
reject_reactions: Vec::default(),
handled: false,
}
}

/// Gets the current state of the promise.
pub(crate) const fn state(&self) -> &PromiseState {
&self.state
Expand Down Expand Up @@ -1266,7 +1271,7 @@ impl Promise {
/// - [ECMAScript reference][spec]
///
/// [spec]: https://tc39.es/ecma262/#sec-createresolvingfunctions
fn create_resolving_functions(
pub(crate) fn create_resolving_functions(
promise: &JsObject,
context: &mut Context<'_>,
) -> ResolvingFunctionsRecord {
Expand Down
22 changes: 20 additions & 2 deletions boa_engine/src/job.rs
Original file line number Diff line number Diff line change
Expand Up @@ -17,14 +17,17 @@
//! [Job]: https://tc39.es/ecma262/#sec-jobs
//! [JobCallback]: https://tc39.es/ecma262/#sec-jobcallback-records

use std::{any::Any, cell::RefCell, collections::VecDeque, fmt::Debug};
use std::{any::Any, cell::RefCell, collections::VecDeque, fmt::Debug, future::Future, pin::Pin};

use crate::{
object::{JsFunction, NativeObject},
Context, JsResult, JsValue,
};
use boa_gc::{Finalize, Trace};

/// The [`Future`] job passed to the [`JobQueue::enqueue_future_job`] operation.
pub type FutureJob = Pin<Box<dyn Future<Output = NativeJob> + 'static>>;

/// An ECMAScript [Job] closure.
///
/// The specification allows scheduling any [`NativeJob`] closure by the host into the job queue.
Expand Down Expand Up @@ -86,7 +89,7 @@ impl NativeJob {
}
}

/// [`JobCallback`][spec] records
/// [`JobCallback`][spec] records.
///
/// [spec]: https://tc39.es/ecma262/#sec-jobcallback-records
#[derive(Trace, Finalize)]
Expand Down Expand Up @@ -150,6 +153,14 @@ pub trait JobQueue {
/// determines if the method should loop until there are no more queued jobs or if
/// it should only run one iteration of the queue.
fn run_jobs(&self, context: &mut Context<'_>);

/// Enqueues a new [`Future`] job on the job queue.
///
/// On completion, `future` returns a new [`NativeJob`] that needs to be enqueued into the
/// job queue to update the state of the inner `Promise`, which is what ECMAScript sees. Failing
/// to do this will leave the inner `Promise` in the `pending` state, which won't call any `then`
/// or `catch` handlers, even if `future` was already completed.
fn enqueue_future_job(&self, future: FutureJob, context: &mut Context<'_>);
}

/// A job queue that does nothing.
Expand All @@ -165,6 +176,8 @@ impl JobQueue for IdleJobQueue {
fn enqueue_promise_job(&self, _: NativeJob, _: &mut Context<'_>) {}

fn run_jobs(&self, _: &mut Context<'_>) {}

fn enqueue_future_job(&self, _: FutureJob, _: &mut Context<'_>) {}
}

/// A simple FIFO job queue that bails on the first error.
Expand Down Expand Up @@ -217,4 +230,9 @@ impl JobQueue for SimpleJobQueue {
next_job = self.0.borrow_mut().pop_front();
}
}

fn enqueue_future_job(&self, future: FutureJob, context: &mut Context<'_>) {
let job = pollster::block_on(future);
self.enqueue_promise_job(job, context);
}
}
101 changes: 100 additions & 1 deletion boa_engine/src/native_function.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,16 @@
//! [`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::{Context, JsResult, JsValue};
use crate::{
builtins::Promise,
job::NativeJob,
object::{JsObject, ObjectData},
Context, JsResult, JsValue,
};

/// The required signature for all native built-in function pointers.
///
Expand Down Expand Up @@ -113,6 +120,98 @@ impl NativeFunction {
}
}

/// Creates a `NativeFunction` from a function returning a [`Future`].
///
/// The returned `NativeFunction` will return an ECMAScript `Promise` that will be fulfilled
/// or rejected when the returned [`Future`] completes.
///
/// # Caveats
///
/// Consider the next snippet:
///
/// ```compile_fail
/// # use boa_engine::{
/// # JsValue,
/// # Context,
/// # JsResult,
/// # NativeFunction
/// # };
/// async fn test(
/// _this: &JsValue,
/// args: &[JsValue],
/// _context: &mut Context<'_>,
/// ) -> JsResult<JsValue> {
/// let arg = args.get(0).cloned();
/// std::future::ready(()).await;
/// drop(arg);
/// Ok(JsValue::null())
/// }
/// 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 ...
///
/// 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.
///
/// In the meantime, a manual desugaring of the async function does the trick:
///
/// ```
/// # use std::future::Future;
/// # use boa_engine::{
/// # JsValue,
/// # Context,
/// # JsResult,
/// # NativeFunction
/// # };
/// fn test(
/// _this: &JsValue,
/// args: &[JsValue],
/// _context: &mut Context<'_>,
/// ) -> impl Future<Output = JsResult<JsValue>> {
/// let arg = args.get(0).cloned();
/// async move {
/// std::future::ready(()).await;
/// drop(arg);
/// Ok(JsValue::null())
/// }
/// }
/// NativeFunction::from_async_fn(test);
/// ```
/// [this issue]: https://github.com/rust-lang/rust/issues/69663
pub fn from_async_fn<Fut>(f: fn(&JsValue, &[JsValue], &mut Context<'_>) -> Fut) -> Self
where
Fut: Future<Output = JsResult<JsValue>> + 'static,
{
Self::from_copy_closure(move |this, args, context| {
let proto = context.intrinsics().constructors().promise().prototype();
let promise = JsObject::from_proto_and_data(proto, ObjectData::promise(Promise::new()));
let resolving_functions = Promise::create_resolving_functions(&promise, context);

let future = f(this, args, context);
let future = async move {
let result = future.await;
NativeJob::new(move |ctx| match result {
Ok(v) => resolving_functions
.resolve
.call(&JsValue::undefined(), &[v], ctx),
Err(e) => {
let e = e.to_opaque(ctx);
resolving_functions
.reject
.call(&JsValue::undefined(), &[e], ctx)
}
})
};
context
.job_queue()
.enqueue_future_job(Box::pin(future), context);
Ok(promise.into())
})
}

/// Creates a `NativeFunction` from a `Copy` closure.
pub fn from_copy_closure<F>(closure: F) -> Self
where
Expand Down
2 changes: 2 additions & 0 deletions boa_examples/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -17,3 +17,5 @@ boa_ast.workspace = true
boa_interner.workspace = true
boa_gc.workspace = true
boa_parser.workspace = true
smol = "1.3.0"
futures-util = "0.3.25"
Loading