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

Use async-fn in traits rather than BoxedFuture #12550

Merged
merged 5 commits into from
Mar 18, 2024

Conversation

ArthurBrussee
Copy link
Contributor

@ArthurBrussee ArthurBrussee commented Mar 18, 2024

Objective

Simplify implementing some asset traits without Box::pin(async move{}) shenanigans.
Fixes (in part) #11308

Solution

Use async-fn in traits when possible in all traits. Traits with return position impl trait are not object safe however, and as AssetReader and AssetWriter are both used with dynamic dispatch, you need a Boxed version of these futures anyway.

In the future, Rust is adding proc macros to generate these traits automatically, and at some point in the future dyn traits should 'just work'. Until then.... this seemed liked the right approach given more ErasedXXX already exist, but, no clue if there's plans here! Especially since these are public now, it's a bit of an unfortunate API, and means this is a breaking change.

In theory this saves some performance when these traits are used with static dispatch, but, seems like most code paths go through dynamic dispatch, which boxes anyway.

I also suspect a bunch of the lifetime annotations on these function could be simplified now as the BoxedFuture was often the only thing returned which needed a lifetime annotation, but I'm not touching that for now as traits + lifetimes can be so tricky.

This is a revival of pull/11362 after a spectacular merge f*ckup, with updates to the latest Bevy. Just to recap some discussion:

  • Overall this seems like a win for code quality, especially when implementing these traits, but a loss for having to deal with ErasedXXX variants.
  • ConditionalSend was the preferred name for the trait that might be Send, to deal with wasm platforms.
  • When reviewing be sure to disable whitespace difference, as that's 95% of the PR.

Changelog

  • AssetReader, AssetWriter, AssetLoader, AssetSaver and Process now use async-fn in traits rather than boxed futures.

Migration Guide

  • Custom implementations of AssetReader, AssetWriter, AssetLoader, AssetSaver and Process should switch to async fn rather than returning a bevy_utils::BoxedFuture.
  • Simultaniously, to use dynamic dispatch on these traits you should instead use dyn ErasedXXX.

Copy link
Contributor

@bushrat011899 bushrat011899 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Excellent work! It's a "small" change being able to use async fn directly for implementing these traits, but every little bit helps in an area already as hairy as async Rust.

Comment on lines 45 to +68
#[cfg(not(target_arch = "wasm32"))]
pub type BoxedFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;
mod conditional_send {
/// Use [`ConditionalSend`] to mark an optional Send trait bound. Useful as on certain platforms (eg. WASM),
/// futures aren't Send.
pub trait ConditionalSend: Send {}
impl<T: Send> ConditionalSend for T {}
}

#[allow(missing_docs)]
#[cfg(target_arch = "wasm32")]
pub type BoxedFuture<'a, T> = Pin<Box<dyn Future<Output = T> + 'a>>;
#[allow(missing_docs)]
mod conditional_send {
pub trait ConditionalSend {}
impl<T> ConditionalSend for T {}
}

pub use conditional_send::*;

/// Use [`ConditionalSendFuture`] for a future with an optional Send trait bound, as on certain platforms (eg. WASM),
/// futures aren't Send.
pub trait ConditionalSendFuture: std::future::Future + ConditionalSend {}
impl<T: std::future::Future + ConditionalSend> ConditionalSendFuture for T {}

/// An owned and dynamically typed Future used when you can't statically type your result or need to add some indirection.
pub type BoxedFuture<'a, T> = std::pin::Pin<Box<dyn ConditionalSendFuture<Output = T> + 'a>>;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As discussed in the previous PR, this is a clean solution to the WASM problem, whilst leaving itself available for other uses as required.

Comment on lines +36 to 46
async fn load<'a>(
&'a self,
reader: &'a mut Reader,
reader: &'a mut Reader<'_>,
_settings: &'a (),
_load_context: &'a mut LoadContext,
) -> BoxedFuture<'a, Result<Self::Asset, Self::Error>> {
Box::pin(async move {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let custom_asset = ron::de::from_bytes::<CustomAsset>(&bytes)?;
Ok(custom_asset)
})
_load_context: &'a mut LoadContext<'_>,
) -> Result<Self::Asset, Self::Error> {
let mut bytes = Vec::new();
reader.read_to_end(&mut bytes).await?;
let custom_asset = ron::de::from_bytes::<CustomAsset>(&bytes)?;
Ok(custom_asset)
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This change alone almost makes the whole PR worth it. A much cleaner option in an area we do expect end-users to work in at some point.


/// Equivalent to an [`AssetWriter`] but using boxed futures, necessary eg. when using a `dyn AssetWriter`,
/// as [`AssetWriter`] isn't currently object safe.
pub trait ErasedAssetWriter: Send + Sync + 'static {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be lovely if (somehow) we could provide a hint like "AssetWriter is not object safe; use ErasedAssetWriter instead." if that particular compiler error is throw. I don't know if that's possible, let alone how, sadly.

Copy link
Contributor Author

@ArthurBrussee ArthurBrussee Mar 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah yeah good idea! Though I'm also not sure how you'd do that :/ I've at least added a note to AssetReader/AssetWriter about it.

@james7132 james7132 added A-Assets Load files from disk to use for things like images, models, and sounds C-Usability A targeted quality-of-life change that makes Bevy easier to use M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide labels Mar 18, 2024
Copy link
Contributor

It looks like your PR is a breaking change, but you didn't provide a migration guide.

Could you add some context on what users should update when this change get released in a new version of Bevy?
It will be used to help writing the migration guide for the version. Putting it after a ## Migration Guide will help it get automatically picked up by our tooling.

Copy link
Member

@james7132 james7132 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks good to me. For other reviewers, this PR looks big but the overwhelming vast majority of it is indentation changes from dropping the Box::pin context. Strong preference from me to expedite since a large change like this tends to accumulate merge conflicts quickly.

@james7132 james7132 added the S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it label Mar 18, 2024
@SolarLiner SolarLiner added the M-Needs-Release-Note Work that should be called out in the blog due to impact label Mar 18, 2024
@alice-i-cecile alice-i-cecile added this pull request to the merge queue Mar 18, 2024
Merged via the queue into bevyengine:main with commit ac49dce Mar 18, 2024
26 checks passed
github-merge-queue bot pushed a commit that referenced this pull request Mar 23, 2024
…async functions) (#12638)

# Objective

get_asset_paths tries to check whether a folder is empty, and if so
delete it. However rather than checking whether any subfolder contains
files it checks whether _all_ subfolders have files.

Also cleanup various BoxedFutures in async recursive functions like
these, rust 1.77 now allows recursive async functions (albeit still by
boxing), hurray! This is a followup to #12550 (sorta). More BoxedFuture
stuff can be removed now that rust 1.77 is out, which can use async
recursive functions! This is mainly just cleaner code wise - the
recursion still boxes the future so not much to win there.

PR is mainly whitespace changes so do disable whitespace diffs for
easier review.
@alice-i-cecile alice-i-cecile removed the M-Needs-Release-Note Work that should be called out in the blog due to impact label May 27, 2024
@mnmaita mnmaita mentioned this pull request Jun 15, 2024
16 tasks
github-merge-queue bot pushed a commit that referenced this pull request Sep 30, 2024
# Objective

Fixes #15541

A bunch of lifetimes were added during the Assets V2 rework, but after
moving to async traits in #12550 they can be elided. That PR mentions
that this might be the case, but apparently it wasn't followed up on at
the time.

~~I ended up grepping for `<'a` and finding a similar case in
`bevy_reflect` which I also fixed.~~ (edit: that one was needed
apparently)

Note that elided lifetimes are unstable in `impl Trait`. If that gets
stabilized then we can elide even more.

## Solution

Remove the extra lifetimes.

## Testing

Everything still compiles. If I have messed something up there is a
small risk that some user code stops compiling, but all the examples
still work at least.

---

## Migration Guide

The traits `AssetLoader`, `AssetSaver` and `Process` traits from
`bevy_asset` now use elided lifetimes. If you implement these then
remove the named lifetime.
robtfm pushed a commit to robtfm/bevy that referenced this pull request Oct 4, 2024
# Objective

Fixes bevyengine#15541

A bunch of lifetimes were added during the Assets V2 rework, but after
moving to async traits in bevyengine#12550 they can be elided. That PR mentions
that this might be the case, but apparently it wasn't followed up on at
the time.

~~I ended up grepping for `<'a` and finding a similar case in
`bevy_reflect` which I also fixed.~~ (edit: that one was needed
apparently)

Note that elided lifetimes are unstable in `impl Trait`. If that gets
stabilized then we can elide even more.

## Solution

Remove the extra lifetimes.

## Testing

Everything still compiles. If I have messed something up there is a
small risk that some user code stops compiling, but all the examples
still work at least.

---

## Migration Guide

The traits `AssetLoader`, `AssetSaver` and `Process` traits from
`bevy_asset` now use elided lifetimes. If you implement these then
remove the named lifetime.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
A-Assets Load files from disk to use for things like images, models, and sounds C-Usability A targeted quality-of-life change that makes Bevy easier to use M-Needs-Migration-Guide A breaking change to Bevy's public API that needs to be noted in a migration guide S-Ready-For-Final-Review This PR has been approved by the community. It's ready for a maintainer to consider merging it
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants