use alloc::fmt;1use core::{2future::Future,3pin::Pin,4task::{Context, Poll},5};67use crate::cfg;89/// Wraps `async_executor::Task`, a spawned future.10///11/// Tasks are also futures themselves and yield the output of the spawned future.12///13/// When a task is dropped, its gets canceled and won't be polled again. To cancel a task a bit14/// more gracefully and wait until it stops running, use the [`Task::cancel()`] method.15///16/// Tasks that panic get immediately canceled. Awaiting a canceled task also causes a panic.17#[must_use = "Tasks are canceled when dropped, use `.detach()` to run them in the background."]18pub struct Task<T>(19cfg::web! {20if {21async_channel::Receiver<Result<T, Panic>>22} else {23async_task::Task<T>24}25},26);2728// Custom constructors for web and non-web platforms29cfg::web! {30if {31impl<T: 'static> Task<T> {32/// Creates a new task by passing the given future to the web33/// runtime as a promise.34pub(crate) fn wrap_future(future: impl Future<Output = T> + 'static) -> Self {35use bevy_platform::exports::wasm_bindgen_futures::spawn_local;36let (sender, receiver) = async_channel::bounded(1);37spawn_local(async move {38// Catch any panics that occur when polling the future so they can39// be propagated back to the task handle.40let value = CatchUnwind(AssertUnwindSafe(future)).await;41let _ = sender.send(value);42});43Self(receiver)44}45}46} else {47impl<T> Task<T> {48/// Creates a new task from a given `async_executor::Task`49pub(crate) fn new(task: async_task::Task<T>) -> Self {50Self(task)51}52}53}54}5556impl<T> Task<T> {57/// Detaches the task to let it keep running in the background.58///59/// # Platform-Specific Behavior60///61/// When building for the web, this method has no effect.62pub fn detach(self) {63cfg::web! {64if {65// Tasks are already treated as detached on the web.66} else {67self.0.detach();68}69}70}7172/// Cancels the task and waits for it to stop running.73///74/// Returns the task's output if it was completed just before it got canceled, or [`None`] if75/// it didn't complete.76///77/// While it's possible to simply drop the [`Task`] to cancel it, this is a cleaner way of78/// canceling because it also waits for the task to stop running.79///80/// # Platform-Specific Behavior81///82/// Canceling tasks is unsupported on the web, and this is the same as awaiting the task.83pub async fn cancel(self) -> Option<T> {84cfg::web! {85if {86// Await the task and handle any panics.87match self.0.recv().await {88Ok(Ok(value)) => Some(value),89Err(_) => None,90Ok(Err(panic)) => {91// drop this to prevent the panic payload from resuming the panic on drop.92// this also leaks the box but I'm not sure how to avoid that93core::mem::forget(panic);94None95}96}97} else {98// Wait for the task to become canceled99self.0.cancel().await100}101}102}103104/// Returns `true` if the current task is finished.105///106/// Unlike poll, it doesn't resolve the final value, it just checks if the task has finished.107/// Note that in a multithreaded environment, this task can be finished immediately after calling this function.108pub fn is_finished(&self) -> bool {109cfg::web! {110if {111// We treat the task as unfinished until the result is sent over the channel.112!self.0.is_empty()113} else {114// Defer to the `async_task` implementation.115self.0.is_finished()116}117}118}119}120121impl<T> Future for Task<T> {122type Output = T;123124cfg::web! {125if {126fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {127// `recv()` returns a future, so we just poll that and hand the result.128let recv = core::pin::pin!(self.0.recv());129match recv.poll(cx) {130Poll::Ready(Ok(Ok(value))) => Poll::Ready(value),131// NOTE: Propagating the panic here sorta has parity with the async_executor behavior.132// For those tasks, polling them after a panic returns a `None` which gets `unwrap`ed, so133// using `resume_unwind` here is essentially keeping the same behavior while adding more information.134Poll::Ready(Ok(Err(_panic))) => crate::cfg::switch! {{135crate::cfg::std => {136std::panic::resume_unwind(_panic)137}138_ => {139unreachable!("catching a panic is only possible with std")140}141}},142Poll::Ready(Err(_)) => panic!("Polled a task after it finished running"),143Poll::Pending => Poll::Pending,144}145}146} else {147fn poll(mut self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {148// `async_task` has `Task` implement `Future`, so we just poll it.149Pin::new(&mut self.0).poll(cx)150}151}152}153}154155// All variants of Task<T> are expected to implement Unpin156impl<T> Unpin for Task<T> {}157158// Derive doesn't work for macro types, so we have to implement this manually.159impl<T> fmt::Debug for Task<T> {160fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {161self.0.fmt(f)162}163}164165// Utilities for catching unwinds on the web.166cfg::web! {167use alloc::boxed::Box;168use core::{169panic::{AssertUnwindSafe, UnwindSafe},170any::Any,171};172173type Panic = Box<dyn Any + Send + 'static>;174175#[pin_project::pin_project]176struct CatchUnwind<F: UnwindSafe>(#[pin] F);177178impl<F: Future + UnwindSafe> Future for CatchUnwind<F> {179type Output = Result<F::Output, Panic>;180fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {181let f = AssertUnwindSafe(|| self.project().0.poll(cx));182183let result = cfg::std! {184if {185std::panic::catch_unwind(f)?186} else {187f()188}189};190191result.map(Ok)192}193}194}195196#[cfg(test)]197mod tests {198use crate::Task;199200#[test]201fn task_is_sync() {202fn is_sync<T: Sync>() {}203is_sync::<Task<()>>();204}205206#[test]207fn task_is_send() {208fn is_send<T: Send>() {}209is_send::<Task<()>>();210}211}212213214