From a072d3ea2baae5d21efebe33cb128b0470705494 Mon Sep 17 00:00:00 2001 From: "Spencer C. Imbleau" Date: Fri, 5 Jul 2024 16:50:01 -0400 Subject: [PATCH] chore: update to bevy 0.14 (#12) * feat: bevy 0.14 * docs: update for 0.2.0 --- .cargo/config.toml | 5 + CHANGELOG.md | 12 +- Cargo.toml | 19 +-- README.md | 24 ++-- examples/blocking.rs | 25 ++-- examples/pool.rs | 8 +- examples/run_wasm/Cargo.toml | 6 + examples/run_wasm/src/main.rs | 14 +++ examples/simple.rs | 8 +- examples/timeout.rs | 8 +- src/lib.rs | 17 +-- src/{native/mod.rs => task.rs} | 136 ++++++++++++++++++--- src/wasm/mod.rs | 213 --------------------------------- 13 files changed, 206 insertions(+), 289 deletions(-) create mode 100644 .cargo/config.toml create mode 100644 examples/run_wasm/Cargo.toml create mode 100644 examples/run_wasm/src/main.rs rename src/{native/mod.rs => task.rs} (62%) delete mode 100644 src/wasm/mod.rs diff --git a/.cargo/config.toml b/.cargo/config.toml new file mode 100644 index 0000000..e2ec9e5 --- /dev/null +++ b/.cargo/config.toml @@ -0,0 +1,5 @@ +[alias] +run_wasm = "run --release --package run_wasm --" +# Other crates use the alias run-wasm, even though crate names should use `_`s not `-`s +# Allow this to be used +run-wasm = "run_wasm" diff --git a/CHANGELOG.md b/CHANGELOG.md index fae19bb..aa152b5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,9 +10,19 @@ Subheadings to categorize changes are `added, changed, deprecated, removed, fixe ## Unreleased +## 0.2.0 + +### Changed + +- Updated to bevy 0.14 + +### Fixed + +- Blocking example for web targets. + ## 0.1.1 -### fixed +### Fixed - READMDE re-uploaded to correct [#10](https://github.com/loopystudios/bevy_async_task/issues/10). diff --git a/Cargo.toml b/Cargo.toml index 2ddc596..0d50a3c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,32 +1,35 @@ +[workspace] +resolver = "2" +members = ["examples/run_wasm"] + [package] name = "bevy_async_task" description = "Ergonomic abstractions to async programming in Bevy" -license = "MIT/Apache-2.0" +license = "Apache-2.0 OR MIT" repository = "https://github.com/loopystudios/bevy_async_task" authors = ["Spencer C. Imbleau"] keywords = ["gamedev", "async"] -version = "0.1.1" +version = "0.2.0" edition = "2021" [lib] [dependencies] -tokio = { version = "1.36.0", default-features = false, features = ["sync"] } -bevy = { version = "0.13", default-features = false, features = [ - "multi-threaded", +tokio = { version = "1.38.0", default-features = false, features = ["sync"] } +bevy = { version = "0.14.0", default-features = false, features = [ + "multi_threaded", ] } -cfg-if = "1.0.0" async-std = "1.12.0" [target.'cfg(not(target_arch = "wasm32"))'.dependencies] -async-compat = "0.2.3" +async-compat = "0.2.4" [dev-dependencies] futures = "0.3.30" futures-timer = "3.0.3" [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies] -tokio = { version = "1.36.0", features = ["full"] } +tokio = { version = "1.38.0", features = ["full"] } [target.'cfg(target_arch = "wasm32")'.dev-dependencies] wasm-bindgen-futures = "0.4.42" diff --git a/README.md b/README.md index 99e99ae..84e4351 100644 --- a/README.md +++ b/README.md @@ -19,12 +19,22 @@ Bevy Async Task provides Bevy system parameters to run asyncronous tasks in the |bevy|bevy_async_task| |---|---| -|0.13|0.1, main| +|0.14|0.2, main| +|0.13|0.1| |<= 0.13|Unsupported| ## Usage -Please see [examples](examples/) for more. +There are several [examples](examples/) for reference. + +You can also run examples on web: + +```shell +# Make sure the Rust toolchain supports the wasm32 target +rustup target add wasm32-unknown-unknown + +cargo run_wasm --example simple +``` ### Polling in systems @@ -40,13 +50,13 @@ fn my_system(mut task_executor: AsyncTaskRunner) { match task_executor.poll() { AsyncTaskStatus::Idle => { task_executor.start(long_task()); - println!("Started new task!"); + info!("Started new task!"); } AsyncTaskStatus::Pending => { // } - AsyncTaskStatus::Finished(v) => { - println!("Received {v}"); + AsnycTaskStatus::Finished(v) => { + info!("Received {v}"); } } } @@ -57,7 +67,7 @@ Poll many similar tasks simultaneously with `AsyncTaskPool`: ```rust fn my_system(mut task_pool: AsyncTaskPool) { if task_pool.is_idle() { - println!("Queueing 5 tasks..."); + info!("Queueing 5 tasks..."); for i in 1..=5 { task_pool.spawn(async move { // Closures work too! sleep(Duration::from_millis(i * 1000)).await; @@ -68,7 +78,7 @@ fn my_system(mut task_pool: AsyncTaskPool) { for status in task_pool.iter_poll() { if let AsyncTaskStatus::Finished(t) = status { - println!("Received {t}"); + info!("Received {t}"); } } } diff --git a/examples/blocking.rs b/examples/blocking.rs index ad094ee..42ebd8d 100644 --- a/examples/blocking.rs +++ b/examples/blocking.rs @@ -1,31 +1,22 @@ -use async_std::task::sleep; -use bevy::prelude::*; +use bevy::{app::PanicHandlerPlugin, log::LogPlugin, prelude::*}; use bevy_async_task::{AsyncTask, AsyncTaskRunner}; -use std::time::Duration; /// You can block with a task runner fn system1(mut task_executor: AsyncTaskRunner) { - let result = task_executor.blocking_recv(async { - sleep(Duration::from_millis(1000)).await; - 1 - }); - println!("Received {result}"); + let result = task_executor.blocking_recv(async { 1 }); + info!("Received {result}"); } /// Or block on a task, without the need of a system parameter. fn system2() { - let result = AsyncTask::new(async { - sleep(Duration::from_millis(1000)).await; - 2 - }) - .blocking_recv(); - println!("Received {result}"); + let result = AsyncTask::new(async { 2 }).blocking_recv(); + info!("Received {result}"); } pub fn main() { App::new() - .add_plugins(MinimalPlugins) - .add_systems(Update, system1) - .add_systems(Update, system2) + .add_plugins((MinimalPlugins, LogPlugin::default(), PanicHandlerPlugin)) + .add_systems(Startup, system2) + .add_systems(Startup, system1) .run(); } diff --git a/examples/pool.rs b/examples/pool.rs index 6f3376c..a19078e 100644 --- a/examples/pool.rs +++ b/examples/pool.rs @@ -1,11 +1,11 @@ use async_std::task::sleep; -use bevy::prelude::*; +use bevy::{app::PanicHandlerPlugin, log::LogPlugin, prelude::*}; use bevy_async_task::{AsyncTaskPool, AsyncTaskStatus}; use std::time::Duration; fn system1(mut task_pool: AsyncTaskPool) { if task_pool.is_idle() { - println!("Queueing 5 tasks..."); + info!("Queueing 5 tasks..."); for i in 1..=5 { task_pool.spawn(async move { sleep(Duration::from_millis(i * 1000)).await; @@ -16,14 +16,14 @@ fn system1(mut task_pool: AsyncTaskPool) { for status in task_pool.iter_poll() { if let AsyncTaskStatus::Finished(t) = status { - println!("Received {t}"); + info!("Received {t}"); } } } pub fn main() { App::new() - .add_plugins(MinimalPlugins) + .add_plugins((MinimalPlugins, LogPlugin::default(), PanicHandlerPlugin)) .add_systems(Update, system1) .run(); } diff --git a/examples/run_wasm/Cargo.toml b/examples/run_wasm/Cargo.toml new file mode 100644 index 0000000..0f5c799 --- /dev/null +++ b/examples/run_wasm/Cargo.toml @@ -0,0 +1,6 @@ +[package] +name = "run_wasm" +publish = false + +[dependencies] +cargo-run-wasm = "0.4.0" diff --git a/examples/run_wasm/src/main.rs b/examples/run_wasm/src/main.rs new file mode 100644 index 0000000..1b832f7 --- /dev/null +++ b/examples/run_wasm/src/main.rs @@ -0,0 +1,14 @@ +/// Use [cargo-run-wasm](https://github.com/rukai/cargo-run-wasm) to build an example for web +/// +/// Usage: +/// ``` +/// cargo run_wasm --example [example_name] +/// ``` +/// Generally: +/// ``` +/// cargo run_wasm --example blocking +/// ``` + +fn main() { + cargo_run_wasm::run_wasm_cli_with_css("body { margin: 0px; }"); +} diff --git a/examples/simple.rs b/examples/simple.rs index 92d43cb..6cd8363 100644 --- a/examples/simple.rs +++ b/examples/simple.rs @@ -1,5 +1,5 @@ use async_std::task::sleep; -use bevy::prelude::*; +use bevy::{app::PanicHandlerPlugin, log::LogPlugin, prelude::*}; use bevy_async_task::{AsyncTaskRunner, AsyncTaskStatus}; use std::time::Duration; @@ -16,20 +16,20 @@ fn my_system(mut task_executor: AsyncTaskRunner) { task_executor.start(long_task()); // Closures also work: // task_executor.start(async { 5 }); - println!("Started!"); + info!("Started!"); } AsyncTaskStatus::Pending => { // Waiting... } AsyncTaskStatus::Finished(v) => { - println!("Received {v}"); + info!("Received {v}"); } } } pub fn main() { App::new() - .add_plugins(MinimalPlugins) + .add_plugins((MinimalPlugins, LogPlugin::default(), PanicHandlerPlugin)) .add_systems(Update, my_system) .run(); } diff --git a/examples/timeout.rs b/examples/timeout.rs index a5ff555..6856a31 100644 --- a/examples/timeout.rs +++ b/examples/timeout.rs @@ -1,20 +1,20 @@ -use bevy::prelude::*; +use bevy::{app::PanicHandlerPlugin, log::LogPlugin, prelude::*}; use bevy_async_task::AsyncTask; use std::time::Duration; /// Use a timeout fn system() { - let _timeout = AsyncTask::<()>::pending() + AsyncTask::<()>::pending() .with_timeout(Duration::from_millis(1000)) .blocking_recv() .unwrap_err(); - println!("Timeout!"); + info!("Timeout!"); } pub fn main() { App::new() - .add_plugins(MinimalPlugins) + .add_plugins((MinimalPlugins, LogPlugin::default(), PanicHandlerPlugin)) .add_systems(Update, system) .run(); } diff --git a/src/lib.rs b/src/lib.rs index 284a365..68929c0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,29 +1,16 @@ #![deny(missing_docs)] //! Ergonomic abstractions to async programming in Bevy for all platforms. -use cfg_if::cfg_if; - mod receiver; +mod task; mod task_pool; mod task_runner; -// Re-exports -pub use async_std::future::TimeoutError; - pub use receiver::AsyncReceiver; +pub use task::{AsyncTask, TimeoutError}; pub use task_pool::AsyncTaskPool; pub use task_runner::AsyncTaskRunner; -cfg_if! { - if #[cfg(target_arch = "wasm32")] { - mod wasm; - pub use wasm::AsyncTask; - } else { - mod native; - pub use native::AsyncTask; - } -} - /// A poll status for an [`AsyncTask`]. pub enum AsyncTaskStatus { /// No task is currently being polled. diff --git a/src/native/mod.rs b/src/task.rs similarity index 62% rename from src/native/mod.rs rename to src/task.rs index 2475ed5..5a49f75 100644 --- a/src/native/mod.rs +++ b/src/task.rs @@ -1,25 +1,30 @@ use crate::AsyncReceiver; +#[cfg(not(target_arch = "wasm32"))] use async_compat::CompatExt; -use async_std::future::{timeout, TimeoutError}; -use std::{future::Future, pin::Pin, time::Duration}; +use async_std::future::timeout; +use bevy::utils::{ConditionalSend, ConditionalSendFuture}; +use std::{future::pending, pin::Pin, time::Duration}; use tokio::sync::oneshot; +// Re-export timeout error +pub use async_std::future::TimeoutError; + /// A wrapper type around an async future. The future may be executed /// asynchronously by an [`AsyncTaskRunner`](crate::AsyncTaskRunner) or /// [`AsyncTaskPool`](crate::AsyncTaskPool), or it may be blocked on the current /// thread. pub struct AsyncTask { - fut: Pin + Send + 'static>>, + fut: Pin + 'static>>, receiver: AsyncReceiver, } impl AsyncTask where - T: Send + 'static, + T: ConditionalSend + 'static, { /// Never resolves to a value or finishes. pub fn pending() -> AsyncTask { - AsyncTask::new(async_std::future::pending::()) + AsyncTask::new(pending()) } /// Add a timeout to the task. @@ -44,11 +49,14 @@ impl AsyncTask { /// Create an async task from a future. pub fn new(fut: F) -> Self where - F: Future + Send + 'static, - F::Output: Send + 'static, + F: ConditionalSendFuture + 'static, + F::Output: ConditionalSend + 'static, { let (tx, rx) = oneshot::channel(); let new_fut = async move { + #[cfg(target_arch = "wasm32")] + let result = fut.await; + #[cfg(not(target_arch = "wasm32"))] let result = fut.compat().await; _ = tx.send(result); }; @@ -63,11 +71,14 @@ impl AsyncTask { /// Create an async task from a future with a timeout. pub fn new_with_timeout(dur: Duration, fut: F) -> AsyncTask> where - F: Future + Send + 'static, - F::Output: Send + 'static, + F: ConditionalSendFuture + 'static, + F::Output: ConditionalSend + 'static, { let (tx, rx) = oneshot::channel(); let new_fut = async move { + #[cfg(target_arch = "wasm32")] + let result = timeout(dur, fut).await; + #[cfg(not(target_arch = "wasm32"))] let result = timeout(dur, fut.compat()).await; _ = tx.send(result); }; @@ -94,7 +105,7 @@ impl AsyncTask { pub fn into_parts( self, ) -> ( - Pin + Send + 'static>>, + Pin + 'static>>, AsyncReceiver, ) { (self.fut, self.receiver) @@ -103,14 +114,15 @@ impl AsyncTask { impl From for AsyncTask where - Fnc: Future + Send + 'static, - Fnc::Output: Send + 'static, + Fnc: ConditionalSendFuture + 'static, + Fnc::Output: ConditionalSend + 'static, { fn from(value: Fnc) -> Self { AsyncTask::new(value) } } +#[cfg(not(target_arch = "wasm32"))] #[cfg(test)] mod test { use super::*; @@ -174,10 +186,7 @@ mod test { #[tokio::test] async fn test_timeout() { - let task = AsyncTask::new_with_timeout( - Duration::from_millis(5), - async_std::future::pending::<()>(), - ); + let task = AsyncTask::new_with_timeout(Duration::from_millis(5), pending::<()>()); let (fut, mut rx) = task.into_parts(); assert_eq!(None, rx.try_recv()); @@ -235,3 +244,98 @@ mod test { } } } + +#[cfg(target_arch = "wasm32")] +#[cfg(test)] +mod test { + use super::*; + use wasm_bindgen::JsValue; + use wasm_bindgen_futures::JsFuture; + use wasm_bindgen_test::wasm_bindgen_test; + + #[wasm_bindgen_test] + async fn test_oneshot() { + let (tx, rx) = oneshot::channel(); + + // Async test + JsFuture::from(wasm_bindgen_futures::future_to_promise(async move { + if tx.send(3).is_err() { + panic!("the receiver dropped"); + } + + match rx.await { + Ok(v) => assert_eq!(3, v), + Err(e) => panic!("the sender dropped ({e})"), + } + + Ok(JsValue::NULL) + })) + .await + .unwrap(); + } + + #[wasm_bindgen_test] + fn test_blocking_recv() { + let task = AsyncTask::new(async move { 5 }); + assert_eq!(5, task.blocking_recv()); + } + + #[wasm_bindgen_test] + async fn test_try_recv() { + let task = AsyncTask::new(async move { 5 }); + let (fut, mut rx) = task.into_parts(); + + assert_eq!(None, rx.try_recv()); + + // Convert to Promise and -await it. + JsFuture::from(wasm_bindgen_futures::future_to_promise(async move { + fut.await; + Ok(JsValue::NULL) + })) + .await + .unwrap(); + + // Spawn + assert_eq!(Some(5), rx.try_recv()); + } + + #[wasm_bindgen_test] + async fn test_timeout() { + let task = AsyncTask::new_with_timeout(Duration::from_millis(5), pending::<()>()); + let (fut, mut rx) = task.into_parts(); + + assert_eq!(None, rx.try_recv()); + + // Convert to Promise and -await it. + JsFuture::from(wasm_bindgen_futures::future_to_promise(async move { + fut.await; + Ok(JsValue::NULL) + })) + .await + .unwrap(); + + // Spawn + let v = rx.try_recv().expect("future loaded no value"); + assert!(v.is_err(), "timeout should have triggered!"); + } + + #[wasm_bindgen_test] + async fn test_with_timeout() { + let task = AsyncTask::<()>::pending().with_timeout(Duration::from_millis(5)); + let (fut, mut rx) = task.into_parts(); + + assert_eq!(None, rx.try_recv()); + + // Convert to Promise and -await it. + JsFuture::from(wasm_bindgen_futures::future_to_promise(async move { + fut.await; + Ok(JsValue::NULL) + })) + .await + .unwrap(); + + // Spawn + let v = rx.try_recv().expect("future loaded no value"); + assert!(v.is_err(), "timeout should have triggered!"); + } +} diff --git a/src/wasm/mod.rs b/src/wasm/mod.rs deleted file mode 100644 index 2fab098..0000000 --- a/src/wasm/mod.rs +++ /dev/null @@ -1,213 +0,0 @@ -use crate::AsyncReceiver; -use async_std::future::{timeout, TimeoutError}; -use std::{future::Future, pin::Pin, time::Duration}; -use tokio::sync::oneshot; - -/// A wrapper type around an async future. The future may be executed -/// asynchronously by an [`AsyncTaskRunner`](crate::AsyncTaskRunner) or -/// [`AsyncTaskPool`](crate::AsyncTaskPool), or it may be blocked on the current -/// thread. -pub struct AsyncTask { - fut: Pin + 'static>>, - receiver: AsyncReceiver, -} - -impl AsyncTask -where - T: 'static, -{ - /// Add a timeout to the task. - pub fn with_timeout(mut self, dur: Duration) -> AsyncTask> { - let (tx, rx) = oneshot::channel(); - let new_fut = async move { - let result = timeout(dur, self.fut) - .await - .map(|_| self.receiver.try_recv().unwrap()); - _ = tx.send(result); - }; - let fut = Box::pin(new_fut); - let receiver = AsyncReceiver { - received: false, - buffer: rx, - }; - AsyncTask::> { fut, receiver } - } -} - -impl AsyncTask -where - T: Send + 'static, -{ - /// Never resolves to a value or finishes. - pub fn pending() -> AsyncTask { - AsyncTask::new(async_std::future::pending::()) - } -} - -impl AsyncTask { - /// Create an async task from a future. - pub fn new(fut: F) -> Self - where - F: Future + 'static, - F::Output: Send + 'static, - { - let (tx, rx) = oneshot::channel(); - let new_fut = async move { - let result = fut.await; - _ = tx.send(result); - }; - let fut = Box::pin(new_fut); - let receiver = AsyncReceiver { - received: false, - buffer: rx, - }; - Self { fut, receiver } - } - - /// Create an async task from a future with a timeout. - pub fn new_with_timeout(dur: Duration, fut: F) -> AsyncTask> - where - F: Future + 'static, - F::Output: Send + 'static, - { - let (tx, rx) = oneshot::channel(); - let new_fut = async move { - let result = timeout(dur, fut).await; - _ = tx.send(result); - }; - let fut = Box::pin(new_fut); - let receiver = AsyncReceiver { - received: false, - buffer: rx, - }; - AsyncTask::> { fut, receiver } - } - - /// Block awaiting the task result. Can only be used outside of async - /// contexts. - pub fn blocking_recv(self) -> T { - let (fut, mut rx) = self.into_parts(); - bevy::tasks::block_on(fut); - rx.buffer.try_recv().unwrap() - } - - /// Break apart the task into a runnable future and the receiver. The - /// receiver is used to catch the output when the runnable is polled. - #[allow(clippy::type_complexity)] - #[must_use] - pub fn into_parts( - self, - ) -> ( - Pin + 'static>>, - AsyncReceiver, - ) { - (self.fut, self.receiver) - } -} - -impl From for AsyncTask -where - Fnc: Future + 'static, - Fnc::Output: Send + 'static, -{ - fn from(value: Fnc) -> Self { - AsyncTask::new(value) - } -} - -#[cfg(test)] -mod test { - use super::*; - use wasm_bindgen::JsValue; - use wasm_bindgen_futures::JsFuture; - use wasm_bindgen_test::wasm_bindgen_test; - - #[wasm_bindgen_test] - async fn test_oneshot() { - let (tx, rx) = oneshot::channel(); - - // Async test - JsFuture::from(wasm_bindgen_futures::future_to_promise(async move { - if tx.send(3).is_err() { - panic!("the receiver dropped"); - } - - match rx.await { - Ok(v) => assert_eq!(3, v), - Err(e) => panic!("the sender dropped ({e})"), - } - - Ok(JsValue::NULL) - })) - .await - .unwrap(); - } - - #[wasm_bindgen_test] - fn test_blocking_recv() { - let task = AsyncTask::new(async move { 5 }); - assert_eq!(5, task.blocking_recv()); - } - - #[wasm_bindgen_test] - async fn test_try_recv() { - let task = AsyncTask::new(async move { 5 }); - let (fut, mut rx) = task.into_parts(); - - assert_eq!(None, rx.try_recv()); - - // Convert to Promise and -await it. - JsFuture::from(wasm_bindgen_futures::future_to_promise(async move { - fut.await; - Ok(JsValue::NULL) - })) - .await - .unwrap(); - - // Spawn - assert_eq!(Some(5), rx.try_recv()); - } - - #[wasm_bindgen_test] - async fn test_timeout() { - let task = AsyncTask::new_with_timeout( - Duration::from_millis(5), - async_std::future::pending::<()>(), - ); - let (fut, mut rx) = task.into_parts(); - - assert_eq!(None, rx.try_recv()); - - // Convert to Promise and -await it. - JsFuture::from(wasm_bindgen_futures::future_to_promise(async move { - fut.await; - Ok(JsValue::NULL) - })) - .await - .unwrap(); - - // Spawn - let v = rx.try_recv().expect("future loaded no value"); - assert!(v.is_err(), "timeout should have triggered!"); - } - - #[wasm_bindgen_test] - async fn test_with_timeout() { - let task = AsyncTask::<()>::pending().with_timeout(Duration::from_millis(5)); - let (fut, mut rx) = task.into_parts(); - - assert_eq!(None, rx.try_recv()); - - // Convert to Promise and -await it. - JsFuture::from(wasm_bindgen_futures::future_to_promise(async move { - fut.await; - Ok(JsValue::NULL) - })) - .await - .unwrap(); - - // Spawn - let v = rx.try_recv().expect("future loaded no value"); - assert!(v.is_err(), "timeout should have triggered!"); - } -}