Skip to content

Add no_std support to bevy_app #16874

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
merged 7 commits into from
Dec 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
62 changes: 54 additions & 8 deletions crates/bevy_app/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -9,31 +9,77 @@ license = "MIT OR Apache-2.0"
keywords = ["bevy"]

[features]
trace = []
bevy_debug_stepping = []
default = ["bevy_reflect"]
default = ["std", "bevy_reflect", "bevy_tasks", "bevy_ecs/default", "downcast"]

# Functionality

## Adds runtime reflection support using `bevy_reflect`.
bevy_reflect = ["dep:bevy_reflect", "bevy_ecs/bevy_reflect"]

## Extends reflection support to functions.
reflect_functions = [
"bevy_reflect",
"bevy_reflect/functions",
"bevy_ecs/reflect_functions",
]

## Adds support for running async background tasks
bevy_tasks = ["dep:bevy_tasks"]

## Adds `downcast-rs` integration for `Plugin`
downcast = ["dep:downcast-rs"]

# Debugging Features

## Enables `tracing` integration, allowing spans and other metrics to be reported
## through that framework.
trace = ["dep:tracing"]

## Provides system stepping support, allowing them to be paused, stepped, and
## other debug operations which can help with diagnosing certain behaviors.
bevy_debug_stepping = []

# Platform Compatibility

## Allows access to the `std` crate. Enabling this feature will prevent compilation
## on `no_std` targets, but provides access to certain additional features on
## supported platforms.
std = [
"bevy_reflect?/std",
"bevy_ecs/std",
"dep:ctrlc",
"downcast-rs?/std",
"bevy_utils/std",
"bevy_tasks?/std",
]

## `critical-section` provides the building blocks for synchronization primitives
## on all platforms, including `no_std`.
critical-section = ["bevy_tasks?/critical-section", "bevy_ecs/critical-section"]

## `portable-atomic` provides additional platform support for atomic types and
## operations, even on targets without native support.
portable-atomic = ["bevy_tasks?/portable-atomic", "bevy_ecs/portable-atomic"]

[dependencies]
# bevy
bevy_derive = { path = "../bevy_derive", version = "0.15.0-dev" }
bevy_ecs = { path = "../bevy_ecs", version = "0.15.0-dev", default-features = false }
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", optional = true }
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev" }
bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev" }
bevy_reflect = { path = "../bevy_reflect", version = "0.15.0-dev", default-features = false, optional = true }
bevy_utils = { path = "../bevy_utils", version = "0.15.0-dev", default-features = false, features = [
"alloc",
] }
bevy_tasks = { path = "../bevy_tasks", version = "0.15.0-dev", default-features = false, optional = true }

# other
downcast-rs = "1.2.0"
downcast-rs = { version = "1.2.0", default-features = false, optional = true }
thiserror = { version = "2", default-features = false }
variadics_please = "1.1"
tracing = { version = "0.1", default-features = false, optional = true }
log = { version = "0.4", default-features = false }

[target.'cfg(not(target_arch = "wasm32"))'.dependencies]
ctrlc = "3.4.4"
ctrlc = { version = "3.4.4", optional = true }

[target.'cfg(target_arch = "wasm32")'.dependencies]
wasm-bindgen = { version = "0.2" }
Expand Down
34 changes: 28 additions & 6 deletions crates/bevy_app/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,10 @@ use crate::{
First, Main, MainSchedulePlugin, PlaceholderPlugin, Plugin, Plugins, PluginsState, SubApp,
SubApps,
};
use alloc::{
boxed::Box,
string::{String, ToString},
};
pub use bevy_derive::AppLabel;
use bevy_ecs::{
component::RequiredComponentsError,
Expand All @@ -11,15 +15,22 @@ use bevy_ecs::{
schedule::{ScheduleBuildSettings, ScheduleLabel},
system::{IntoObserverSystem, SystemId, SystemInput},
};
#[cfg(feature = "trace")]
use bevy_utils::tracing::info_span;
use bevy_utils::{tracing::debug, HashMap};
use bevy_utils::HashMap;
use core::{fmt::Debug, num::NonZero, panic::AssertUnwindSafe};
use log::debug;
use thiserror::Error;

#[cfg(feature = "trace")]
use tracing::info_span;
Comment on lines +23 to +24
Copy link
Contributor Author

Choose a reason for hiding this comment

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

As with bevy_ecs, I'm flattening our relationship with bevy_utils::tracing and also relying on log for non-span logging. It's already been mentioned that we should probably move things like log and tracing into workspace dependencies, but I'm just maintaining status quo for now.


#[cfg(feature = "std")]
use std::{
panic::{catch_unwind, resume_unwind},
process::{ExitCode, Termination},
};
use thiserror::Error;

#[cfg(feature = "downcast")]
use alloc::vec::Vec;

bevy_ecs::define_label!(
/// A strongly-typed class of labels used to identify an [`App`].
Expand Down Expand Up @@ -458,12 +469,21 @@ impl App {
.push(Box::new(PlaceholderPlugin));

self.main_mut().plugin_build_depth += 1;
let result = catch_unwind(AssertUnwindSafe(|| plugin.build(self)));

let f = AssertUnwindSafe(|| plugin.build(self));

#[cfg(feature = "std")]
let result = catch_unwind(f);

#[cfg(not(feature = "std"))]
f();

self.main_mut()
.plugin_names
.insert(plugin.name().to_string());
self.main_mut().plugin_build_depth -= 1;

#[cfg(feature = "std")]
if let Err(payload) = result {
resume_unwind(payload);
}
Expand Down Expand Up @@ -499,6 +519,7 @@ impl App {
/// # app.add_plugins(ImagePlugin::default());
/// let default_sampler = app.get_added_plugins::<ImagePlugin>()[0].default_sampler;
/// ```
#[cfg(feature = "downcast")]
pub fn get_added_plugins<T>(&self) -> Vec<&T>
where
T: Plugin,
Expand Down Expand Up @@ -1294,7 +1315,7 @@ type RunnerFn = Box<dyn FnOnce(App) -> AppExit>;

fn run_once(mut app: App) -> AppExit {
while app.plugins_state() == PluginsState::Adding {
#[cfg(not(target_arch = "wasm32"))]
#[cfg(all(not(target_arch = "wasm32"), feature = "bevy_tasks"))]
bevy_tasks::tick_global_task_pools_on_main_thread();
}
app.finish();
Expand Down Expand Up @@ -1364,6 +1385,7 @@ impl From<u8> for AppExit {
}
}

#[cfg(feature = "std")]
impl Termination for AppExit {
fn report(self) -> ExitCode {
match self {
Expand Down
5 changes: 3 additions & 2 deletions crates/bevy_app/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
html_logo_url = "https://bevyengine.org/assets/icon.png",
html_favicon_url = "https://bevyengine.org/assets/icon.png"
)]
#![cfg_attr(not(feature = "std"), no_std)]

//! This crate is about everything concerning the highest-level, application layer of a Bevy app.

Expand All @@ -23,7 +24,7 @@ mod plugin;
mod plugin_group;
mod schedule_runner;
mod sub_app;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(all(not(target_arch = "wasm32"), feature = "std"))]
mod terminal_ctrl_c_handler;
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Can't use Control-C in the terminal if your platform doesn't have a terminal...or a keyboard...


pub use app::*;
Expand All @@ -33,7 +34,7 @@ pub use plugin::*;
pub use plugin_group::*;
pub use schedule_runner::*;
pub use sub_app::*;
#[cfg(not(target_arch = "wasm32"))]
#[cfg(all(not(target_arch = "wasm32"), feature = "std"))]
pub use terminal_ctrl_c_handler::*;

/// The app prelude.
Expand Down
1 change: 1 addition & 0 deletions crates/bevy_app/src/main_schedule.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
use crate::{App, Plugin};
use alloc::{vec, vec::Vec};
use bevy_ecs::{
schedule::{
ExecutorKind, InternedScheduleLabel, IntoSystemSetConfigs, Schedule, ScheduleLabel,
Expand Down
15 changes: 15 additions & 0 deletions crates/bevy_app/src/plugin.rs
Original file line number Diff line number Diff line change
@@ -1,8 +1,21 @@
// TODO: Upstream `portable-atomic` support to `downcast_rs` and unconditionally
// include it as a dependency.
// See https://github.com/marcianx/downcast-rs/pull/22 for details
#[cfg(feature = "downcast")]
use downcast_rs::{impl_downcast, Downcast};

use crate::App;
use core::any::Any;

/// Dummy trait with the same name as `downcast_rs::Downcast`. This is to ensure
/// the `Plugin: Downcast` bound can remain even when `downcast` isn't enabled.
#[cfg(not(feature = "downcast"))]
#[doc(hidden)]
pub trait Downcast {}

#[cfg(not(feature = "downcast"))]
impl<T: ?Sized> Downcast for T {}
Comment on lines +12 to +17
Copy link
Contributor Author

Choose a reason for hiding this comment

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

downcast-rs isn't compatible with portablie-atomic (though it could be with a PR I believe!), so I've included a little dummy trait so the Plugin: Downcast bound can remain.


/// A collection of Bevy app logic and configuration.
///
/// Plugins configure an [`App`]. When an [`App`] registers a plugin,
Expand Down Expand Up @@ -92,6 +105,7 @@ pub trait Plugin: Downcast + Any + Send + Sync {
}
}

#[cfg(feature = "downcast")]
impl_downcast!(Plugin);

impl<T: Fn(&mut App) + Send + Sync + 'static> Plugin for T {
Expand Down Expand Up @@ -129,6 +143,7 @@ pub trait Plugins<Marker>: sealed::Plugins<Marker> {}
impl<Marker, T> Plugins<Marker> for T where T: sealed::Plugins<Marker> {}

mod sealed {
use alloc::boxed::Box;
use variadics_please::all_tuples;

use crate::{App, AppError, Plugin, PluginGroup};
Expand Down
9 changes: 6 additions & 3 deletions crates/bevy_app/src/plugin_group.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,12 @@
use crate::{App, AppError, Plugin};
use bevy_utils::{
tracing::{debug, warn},
TypeIdMap,
use alloc::{
boxed::Box,
string::{String, ToString},
vec::Vec,
};
use bevy_utils::TypeIdMap;
use core::any::TypeId;
use log::{debug, warn};

/// A macro for generating a well-documented [`PluginGroup`] from a list of [`Plugin`] paths.
///
Expand Down
19 changes: 14 additions & 5 deletions crates/bevy_app/src/schedule_runner.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,10 @@ use crate::{
plugin::Plugin,
PluginsState,
};
use bevy_utils::{Duration, Instant};
use bevy_utils::Duration;

#[cfg(any(target_arch = "wasm32", feature = "std"))]
use bevy_utils::Instant;
Comment on lines +8 to +9
Copy link
Contributor Author

Choose a reason for hiding this comment

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

I'm not a fan of how we bring this Instant in. I think it'd be worth considering some kind of bevy_platform_support crate which moves things like spin, portable-atomic, critical-section, wasm-time, etc. into a single spot. It'd be more focussed than bevy_utils, and possibly clean up some of the cfg(...) noise we have across Bevy.

Copy link
Member

Choose a reason for hiding this comment

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

Yes please!


#[cfg(target_arch = "wasm32")]
use {
Expand Down Expand Up @@ -76,7 +79,7 @@ impl Plugin for ScheduleRunnerPlugin {
let plugins_state = app.plugins_state();
if plugins_state != PluginsState::Cleaned {
while app.plugins_state() == PluginsState::Adding {
#[cfg(not(target_arch = "wasm32"))]
#[cfg(all(not(target_arch = "wasm32"), feature = "bevy_tasks"))]
bevy_tasks::tick_global_task_pools_on_main_thread();
}
app.finish();
Expand All @@ -95,8 +98,9 @@ impl Plugin for ScheduleRunnerPlugin {
}
RunMode::Loop { wait } => {
let tick = move |app: &mut App,
wait: Option<Duration>|
_wait: Option<Duration>|
-> Result<Option<Duration>, AppExit> {
#[cfg(any(target_arch = "wasm32", feature = "std"))]
let start_time = Instant::now();

app.update();
Expand All @@ -105,9 +109,11 @@ impl Plugin for ScheduleRunnerPlugin {
return Err(exit);
};

#[cfg(any(target_arch = "wasm32", feature = "std"))]
let end_time = Instant::now();

if let Some(wait) = wait {
#[cfg(any(target_arch = "wasm32", feature = "std"))]
if let Some(wait) = _wait {
let exe_time = end_time - start_time;
if exe_time < wait {
return Ok(Some(wait - exe_time));
Expand All @@ -121,7 +127,10 @@ impl Plugin for ScheduleRunnerPlugin {
{
loop {
match tick(&mut app, wait) {
Ok(Some(delay)) => std::thread::sleep(delay),
Ok(Some(_delay)) => {
#[cfg(feature = "std")]
std::thread::sleep(_delay);
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Without access to std::thread::sleep, no_std apps will run as-fast-as-possible by default. This isn't ideal, but I believe a reasonable compromise.

}
Ok(None) => continue,
Err(exit) => return exit,
}
Expand Down
8 changes: 5 additions & 3 deletions crates/bevy_app/src/sub_app.rs
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
use crate::{App, AppLabel, InternedAppLabel, Plugin, Plugins, PluginsState};
use alloc::{boxed::Box, string::String, vec::Vec};
use bevy_ecs::{
event::EventRegistry,
prelude::*,
schedule::{InternedScheduleLabel, ScheduleBuildSettings, ScheduleLabel},
system::{SystemId, SystemInput},
};

#[cfg(feature = "trace")]
use bevy_utils::tracing::info_span;
use bevy_utils::{HashMap, HashSet};
use core::fmt::Debug;

#[cfg(feature = "trace")]
use tracing::info_span;

type ExtractFn = Box<dyn Fn(&mut World, &mut World) + Send>;

/// A secondary application with its own [`World`]. These can run independently of each other.
Expand Down Expand Up @@ -332,6 +333,7 @@ impl SubApp {
}

/// See [`App::get_added_plugins`].
#[cfg(feature = "downcast")]
pub fn get_added_plugins<T>(&self) -> Vec<&T>
Copy link
Contributor Author

Choose a reason for hiding this comment

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

This is the only real consequence of losing downcast-rs support on certain no_std targets (not all!). I would like to bring feature parity regardless, but it is fairly minor IMO.

where
T: Plugin,
Expand Down
4 changes: 2 additions & 2 deletions crates/bevy_app/src/terminal_ctrl_c_handler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -63,9 +63,9 @@ impl Plugin for TerminalCtrlCHandlerPlugin {
match result {
Ok(()) => {}
Err(ctrlc::Error::MultipleHandlers) => {
bevy_utils::tracing::info!("Skipping installing `Ctrl+C` handler as one was already installed. Please call `TerminalCtrlCHandlerPlugin::gracefully_exit` in your own `Ctrl+C` handler if you want Bevy to gracefully exit on `Ctrl+C`.");
log::info!("Skipping installing `Ctrl+C` handler as one was already installed. Please call `TerminalCtrlCHandlerPlugin::gracefully_exit` in your own `Ctrl+C` handler if you want Bevy to gracefully exit on `Ctrl+C`.");
}
Err(err) => bevy_utils::tracing::warn!("Failed to set `Ctrl+C` handler: {err}"),
Err(err) => log::warn!("Failed to set `Ctrl+C` handler: {err}"),
}

app.add_systems(Update, TerminalCtrlCHandlerPlugin::exit_on_flag);
Expand Down
5 changes: 3 additions & 2 deletions crates/bevy_ecs/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ std = [
## on all platforms, including `no_std`.
critical-section = [
"dep:critical-section",
"bevy_tasks/critical-section",
"bevy_tasks?/critical-section",
"portable-atomic?/critical-section",
]

Expand All @@ -88,8 +88,9 @@ critical-section = [
portable-atomic = [
"dep:portable-atomic",
"dep:portable-atomic-util",
"bevy_tasks/portable-atomic",
"bevy_tasks?/portable-atomic",
"concurrent-queue/portable-atomic",
"spin/portable_atomic",
Copy link
Contributor Author

Choose a reason for hiding this comment

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

The changes to bevy_ecs/Cargo.toml could be split into their own PR, but for now I'm just including them here for brevity. I'm happy to split it out if anyone has any concerns.

]

[dependencies]
Expand Down
8 changes: 8 additions & 0 deletions tools/ci/src/commands/compile_check_no_std.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,14 @@ impl Prepare for CompileCheckNoStdCommand {
"Please fix compiler errors in output above for bevy_ecs no_std compatibility.",
));

commands.push(PreparedCommand::new::<Self>(
cmd!(
sh,
"cargo check -p bevy_app --no-default-features --features bevy_reflect --target {target}"
Copy link
Contributor Author

Choose a reason for hiding this comment

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

Now that bevy_app is compatible, we can start looking into replacing/augmenting this compile_check_no_std CI task with something more robust and tangible, like a no_std example crate.

),
"Please fix compiler errors in output above for bevy_app no_std compatibility.",
));

commands
}
}
Loading