Skip to content

Commit 72f096c

Browse files
bushrat011899hymmalice-i-cecile
authored
Add no_std support to bevy_tasks (#15464)
# Objective - Contributes to #15460 ## Solution - Added the following features: - `std` (default) - `async_executor` (default) - `edge_executor` - `critical-section` - `portable-atomic` - Added [`edge-executor`](https://crates.io/crates/edge-executor) as a `no_std` alternative to `async-executor`. - Updated the `single_threaded_task_pool` to work in `no_std` environments by gating its reliance on `thread_local`. ## Testing - Added to `compile-check-no-std` CI command ## Notes - In previous iterations of this PR, a custom `async-executor` alternative was vendored in. This raised concerns around maintenance and testing. In this iteration, an existing version of that same vendoring is now used, but _only_ in `no_std` contexts. For existing `std` contexts, the original `async-executor` is used. - Due to the way statics work, certain `TaskPool` operations have added restrictions around `Send`/`Sync` in `no_std`. This is because there isn't a straightforward way to create a thread-local in `no_std`. If these added constraints pose an issue we can revisit this at a later date. - If a user enables both the `async_executor` and `edge_executor` features, we will default to using `async-executor`. Since enabling `async_executor` requires `std`, we can safely assume we are in an `std` context and use the original library. --------- Co-authored-by: Mike <[email protected]> Co-authored-by: Alice Cecile <[email protected]>
1 parent bc572cd commit 72f096c

13 files changed

+313
-59
lines changed

crates/bevy_tasks/Cargo.toml

+46-3
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,57 @@ license = "MIT OR Apache-2.0"
99
keywords = ["bevy"]
1010

1111
[features]
12-
multi_threaded = ["dep:async-channel", "dep:concurrent-queue"]
12+
default = ["std", "async_executor"]
13+
std = [
14+
"futures-lite/std",
15+
"async-task/std",
16+
"spin/std",
17+
"edge-executor?/std",
18+
"portable-atomic-util?/std",
19+
]
20+
multi_threaded = ["std", "dep:async-channel", "dep:concurrent-queue"]
21+
async_executor = ["std", "dep:async-executor"]
22+
edge_executor = ["dep:edge-executor"]
23+
critical-section = [
24+
"dep:critical-section",
25+
"edge-executor?/critical-section",
26+
"portable-atomic?/critical-section",
27+
]
28+
portable-atomic = [
29+
"dep:portable-atomic",
30+
"dep:portable-atomic-util",
31+
"edge-executor?/portable-atomic",
32+
"async-task/portable-atomic",
33+
"spin/portable_atomic",
34+
]
1335

1436
[dependencies]
15-
futures-lite = "2.0.1"
16-
async-executor = "1.11"
37+
futures-lite = { version = "2.0.1", default-features = false, features = [
38+
"alloc",
39+
] }
40+
async-task = { version = "4.4.0", default-features = false }
41+
spin = { version = "0.9.8", default-features = false, features = [
42+
"spin_mutex",
43+
"rwlock",
44+
"once",
45+
] }
46+
derive_more = { version = "1", default-features = false, features = [
47+
"deref",
48+
"deref_mut",
49+
] }
50+
51+
async-executor = { version = "1.11", optional = true }
52+
edge-executor = { version = "0.4.1", default-features = false, optional = true }
1753
async-channel = { version = "2.3.0", optional = true }
1854
async-io = { version = "2.0.0", optional = true }
1955
concurrent-queue = { version = "2.0.0", optional = true }
56+
critical-section = { version = "1.2.0", optional = true }
57+
portable-atomic = { version = "1", default-features = false, features = [
58+
"fallback",
59+
], optional = true }
60+
portable-atomic-util = { version = "0.2.4", features = [
61+
"alloc",
62+
], optional = true }
2063

2164
[target.'cfg(target_arch = "wasm32")'.dependencies]
2265
wasm-bindgen-futures = "0.4"

crates/bevy_tasks/README.md

+4
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ The determining factor for what kind of work should go in each pool is latency r
3434
await receiving data from somewhere (i.e. disk) and signal other systems when the data is ready
3535
for consumption. (likely via channels)
3636

37+
## `no_std` Support
38+
39+
To enable `no_std` support in this crate, you will need to disable default features, and enable the `edge_executor` and `critical-section` features. For platforms without full support for Rust atomics, you may also need to enable the `portable-atomic` feature.
40+
3741
[bevy]: https://bevyengine.org
3842
[rayon]: https://github.com/rayon-rs/rayon
3943
[async-executor]: https://github.com/stjepang/async-executor

crates/bevy_tasks/src/executor.rs

+84
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,84 @@
1+
//! Provides a fundamental executor primitive appropriate for the target platform
2+
//! and feature set selected.
3+
//! By default, the `async_executor` feature will be enabled, which will rely on
4+
//! [`async-executor`] for the underlying implementation. This requires `std`,
5+
//! so is not suitable for `no_std` contexts. Instead, you must use `edge_executor`,
6+
//! which relies on the alternate [`edge-executor`] backend.
7+
//!
8+
//! [`async-executor`]: https://crates.io/crates/async-executor
9+
//! [`edge-executor`]: https://crates.io/crates/edge-executor
10+
11+
pub use async_task::Task;
12+
use core::{
13+
fmt,
14+
panic::{RefUnwindSafe, UnwindSafe},
15+
};
16+
use derive_more::{Deref, DerefMut};
17+
18+
#[cfg(feature = "multi_threaded")]
19+
pub use async_task::FallibleTask;
20+
21+
#[cfg(feature = "async_executor")]
22+
type ExecutorInner<'a> = async_executor::Executor<'a>;
23+
24+
#[cfg(feature = "async_executor")]
25+
type LocalExecutorInner<'a> = async_executor::LocalExecutor<'a>;
26+
27+
#[cfg(all(not(feature = "async_executor"), feature = "edge_executor"))]
28+
type ExecutorInner<'a> = edge_executor::Executor<'a, 64>;
29+
30+
#[cfg(all(not(feature = "async_executor"), feature = "edge_executor"))]
31+
type LocalExecutorInner<'a> = edge_executor::LocalExecutor<'a, 64>;
32+
33+
/// Wrapper around a multi-threading-aware async executor.
34+
/// Spawning will generally require tasks to be `Send` and `Sync` to allow multiple
35+
/// threads to send/receive/advance tasks.
36+
///
37+
/// If you require an executor _without_ the `Send` and `Sync` requirements, consider
38+
/// using [`LocalExecutor`] instead.
39+
#[derive(Deref, DerefMut, Default)]
40+
pub struct Executor<'a>(ExecutorInner<'a>);
41+
42+
/// Wrapper around a single-threaded async executor.
43+
/// Spawning wont generally require tasks to be `Send` and `Sync`, at the cost of
44+
/// this executor itself not being `Send` or `Sync`. This makes it unsuitable for
45+
/// global statics.
46+
///
47+
/// If need to store an executor in a global static, or send across threads,
48+
/// consider using [`Executor`] instead.
49+
#[derive(Deref, DerefMut, Default)]
50+
pub struct LocalExecutor<'a>(LocalExecutorInner<'a>);
51+
52+
impl Executor<'_> {
53+
/// Construct a new [`Executor`]
54+
#[allow(dead_code, reason = "not all feature flags require this function")]
55+
pub const fn new() -> Self {
56+
Self(ExecutorInner::new())
57+
}
58+
}
59+
60+
impl LocalExecutor<'_> {
61+
/// Construct a new [`LocalExecutor`]
62+
#[allow(dead_code, reason = "not all feature flags require this function")]
63+
pub const fn new() -> Self {
64+
Self(LocalExecutorInner::new())
65+
}
66+
}
67+
68+
impl UnwindSafe for Executor<'_> {}
69+
impl RefUnwindSafe for Executor<'_> {}
70+
71+
impl UnwindSafe for LocalExecutor<'_> {}
72+
impl RefUnwindSafe for LocalExecutor<'_> {}
73+
74+
impl fmt::Debug for Executor<'_> {
75+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
76+
f.debug_struct("Executor").finish()
77+
}
78+
}
79+
80+
impl fmt::Debug for LocalExecutor<'_> {
81+
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
82+
f.debug_struct("LocalExecutor").finish()
83+
}
84+
}

crates/bevy_tasks/src/iter/adapters.rs

+13
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,7 @@
11
use crate::iter::ParallelIterator;
22

3+
/// Chains two [`ParallelIterator`]s `T` and `U`, first returning
4+
/// batches from `T`, and then from `U`.
35
#[derive(Debug)]
46
pub struct Chain<T, U> {
57
pub(crate) left: T,
@@ -24,6 +26,7 @@ where
2426
}
2527
}
2628

29+
/// Maps a [`ParallelIterator`] `P` using the provided function `F`.
2730
#[derive(Debug)]
2831
pub struct Map<P, F> {
2932
pub(crate) iter: P,
@@ -41,6 +44,7 @@ where
4144
}
4245
}
4346

47+
/// Filters a [`ParallelIterator`] `P` using the provided predicate `F`.
4448
#[derive(Debug)]
4549
pub struct Filter<P, F> {
4650
pub(crate) iter: P,
@@ -60,6 +64,7 @@ where
6064
}
6165
}
6266

67+
/// Filter-maps a [`ParallelIterator`] `P` using the provided function `F`.
6368
#[derive(Debug)]
6469
pub struct FilterMap<P, F> {
6570
pub(crate) iter: P,
@@ -77,6 +82,7 @@ where
7782
}
7883
}
7984

85+
/// Flat-maps a [`ParallelIterator`] `P` using the provided function `F`.
8086
#[derive(Debug)]
8187
pub struct FlatMap<P, F> {
8288
pub(crate) iter: P,
@@ -98,6 +104,7 @@ where
98104
}
99105
}
100106

107+
/// Flattens a [`ParallelIterator`] `P`.
101108
#[derive(Debug)]
102109
pub struct Flatten<P> {
103110
pub(crate) iter: P,
@@ -117,6 +124,8 @@ where
117124
}
118125
}
119126

127+
/// Fuses a [`ParallelIterator`] `P`, ensuring once it returns [`None`] once, it always
128+
/// returns [`None`].
120129
#[derive(Debug)]
121130
pub struct Fuse<P> {
122131
pub(crate) iter: Option<P>,
@@ -138,6 +147,7 @@ where
138147
}
139148
}
140149

150+
/// Inspects a [`ParallelIterator`] `P` using the provided function `F`.
141151
#[derive(Debug)]
142152
pub struct Inspect<P, F> {
143153
pub(crate) iter: P,
@@ -155,6 +165,7 @@ where
155165
}
156166
}
157167

168+
/// Copies a [`ParallelIterator`] `P`'s returned values.
158169
#[derive(Debug)]
159170
pub struct Copied<P> {
160171
pub(crate) iter: P,
@@ -171,6 +182,7 @@ where
171182
}
172183
}
173184

185+
/// Clones a [`ParallelIterator`] `P`'s returned values.
174186
#[derive(Debug)]
175187
pub struct Cloned<P> {
176188
pub(crate) iter: P,
@@ -187,6 +199,7 @@ where
187199
}
188200
}
189201

202+
/// Cycles a [`ParallelIterator`] `P` indefinitely.
190203
#[derive(Debug)]
191204
pub struct Cycle<P> {
192205
pub(crate) iter: P,

crates/bevy_tasks/src/iter/mod.rs

+1
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
use crate::TaskPool;
2+
use alloc::vec::Vec;
23

34
mod adapters;
45
pub use adapters::*;

crates/bevy_tasks/src/lib.rs

+20-3
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@
44
html_logo_url = "https://bevyengine.org/assets/icon.png",
55
html_favicon_url = "https://bevyengine.org/assets/icon.png"
66
)]
7+
#![cfg_attr(not(feature = "std"), no_std)]
78

89
extern crate alloc;
910

11+
mod executor;
12+
1013
mod slice;
1114
pub use slice::{ParallelSlice, ParallelSliceMut};
1215

@@ -37,9 +40,9 @@ mod thread_executor;
3740
#[cfg(all(not(target_arch = "wasm32"), feature = "multi_threaded"))]
3841
pub use thread_executor::{ThreadExecutor, ThreadExecutorTicker};
3942

40-
#[cfg(feature = "async-io")]
43+
#[cfg(all(feature = "async-io", feature = "std"))]
4144
pub use async_io::block_on;
42-
#[cfg(not(feature = "async-io"))]
45+
#[cfg(all(not(feature = "async-io"), feature = "std"))]
4346
pub use futures_lite::future::block_on;
4447
pub use futures_lite::future::poll_once;
4548

@@ -54,13 +57,17 @@ pub use futures_lite;
5457
pub mod prelude {
5558
#[doc(hidden)]
5659
pub use crate::{
57-
block_on,
5860
iter::ParallelIterator,
5961
slice::{ParallelSlice, ParallelSliceMut},
6062
usages::{AsyncComputeTaskPool, ComputeTaskPool, IoTaskPool},
6163
};
64+
65+
#[cfg(feature = "std")]
66+
#[doc(hidden)]
67+
pub use crate::block_on;
6268
}
6369

70+
#[cfg(feature = "std")]
6471
use core::num::NonZero;
6572

6673
/// Gets the logical CPU core count available to the current process.
@@ -69,8 +76,18 @@ use core::num::NonZero;
6976
/// it will return a default value of 1 if it internally errors out.
7077
///
7178
/// This will always return at least 1.
79+
#[cfg(feature = "std")]
7280
pub fn available_parallelism() -> usize {
7381
std::thread::available_parallelism()
7482
.map(NonZero::<usize>::get)
7583
.unwrap_or(1)
7684
}
85+
86+
/// Gets the logical CPU core count available to the current process.
87+
///
88+
/// This will always return at least 1.
89+
#[cfg(not(feature = "std"))]
90+
pub fn available_parallelism() -> usize {
91+
// Without access to std, assume a single thread is available
92+
1
93+
}

0 commit comments

Comments
 (0)