From 958c9bb652562e0abcc3ba32cd3c56ac633c542f Mon Sep 17 00:00:00 2001 From: Zachary Harrold Date: Tue, 18 Mar 2025 11:45:25 +1100 Subject: [PATCH] Add `no_std` Library Example (#18333) # Objective - Fixes #17506 - Fixes #16258 ## Solution - Added a new folder of examples, `no_std`, similar to the `mobile` folder. - Added a single example, `no_std_library`, which demonstrates how to make a `no_std` compatible Bevy library. - Added a new CI task, `check-compiles-no-std-examples`, which checks that `no_std` examples compile on `no_std` targets. - Added `bevy_platform_support::prelude` to `bevy::prelude`. ## Testing - CI --- ## Notes - I've structured the folders here to permit further `no_std` examples (e.g., GameBoy Games, ESP32 firmware, etc.), but I am starting with the simplest and least controversial example. - I've tried to be as clear as possible with the documentation for this example, catering to an audience who may not have even heard of `no_std` before. --------- Co-authored-by: Greeble <166992735+greeble-dev@users.noreply.github.com> --- .github/workflows/ci.yml | 25 +++++ Cargo.toml | 14 +++ crates/bevy_internal/src/prelude.rs | 4 +- examples/README.md | 7 ++ examples/no_std/README.md | 18 ++++ examples/no_std/library/Cargo.toml | 45 +++++++++ examples/no_std/library/README.md | 64 +++++++++++++ examples/no_std/library/src/lib.rs | 137 ++++++++++++++++++++++++++++ 8 files changed, 312 insertions(+), 2 deletions(-) create mode 100644 examples/no_std/README.md create mode 100644 examples/no_std/library/Cargo.toml create mode 100644 examples/no_std/library/README.md create mode 100644 examples/no_std/library/src/lib.rs diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 29d6e2418c..4f82c0d0ea 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -177,6 +177,31 @@ jobs: - name: Check Compile run: cargo check -p bevy --no-default-features --features default_no_std --target thumbv6m-none-eabi + check-compiles-no-std-examples: + runs-on: ubuntu-latest + timeout-minutes: 30 + needs: ci + steps: + - uses: actions/checkout@v4 + - uses: actions/cache@v4 + with: + path: | + ~/.cargo/bin/ + ~/.cargo/registry/index/ + ~/.cargo/registry/cache/ + ~/.cargo/git/db/ + target/ + crates/bevy_ecs_compile_fail_tests/target/ + crates/bevy_reflect_compile_fail_tests/target/ + key: ${{ runner.os }}-cargo-check-compiles-no-std-examples-${{ hashFiles('**/Cargo.toml') }} + - uses: dtolnay/rust-toolchain@stable + with: + targets: x86_64-unknown-none + - name: Install Linux dependencies + uses: ./.github/actions/install-linux-deps + - name: Check Compile + run: cd examples/no_std/library && cargo check --no-default-features --features libm,critical-section --target x86_64-unknown-none + build-wasm: runs-on: ubuntu-latest timeout-minutes: 30 diff --git a/Cargo.toml b/Cargo.toml index 6da9dd703d..4efd0f5d87 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,8 @@ members = [ "crates/bevy_reflect/compile_fail", # Examples of compiling Bevy for mobile platforms. "examples/mobile", + # Examples of using Bevy on no_std platforms. + "examples/no_std/*", # Benchmarks "benches", # Internal tools that are not published. @@ -4287,3 +4289,15 @@ name = "Widgets" description = "Example UI Widgets" category = "Helpers" wasm = true + +[[example]] +name = "no_std_library" +path = "examples/no_std/library/src/lib.rs" +doc-scrape-examples = true +crate-type = ["lib"] + +[package.metadata.example.no_std_library] +name = "`no_std` Compatible Library" +description = "Example library compatible with `std` and `no_std` targets" +category = "Embedded" +wasm = true diff --git a/crates/bevy_internal/src/prelude.rs b/crates/bevy_internal/src/prelude.rs index 9f7dd8f33e..a01cc4da2c 100644 --- a/crates/bevy_internal/src/prelude.rs +++ b/crates/bevy_internal/src/prelude.rs @@ -1,7 +1,7 @@ #[doc(hidden)] pub use crate::{ - app::prelude::*, ecs::prelude::*, reflect::prelude::*, time::prelude::*, utils::prelude::*, - DefaultPlugins, MinimalPlugins, + app::prelude::*, ecs::prelude::*, platform_support::prelude::*, reflect::prelude::*, + time::prelude::*, utils::prelude::*, DefaultPlugins, MinimalPlugins, }; #[doc(hidden)] diff --git a/examples/README.md b/examples/README.md index 053e522ab9..35fac7c16c 100644 --- a/examples/README.md +++ b/examples/README.md @@ -49,6 +49,7 @@ git checkout v0.4.0 - [Dev tools](#dev-tools) - [Diagnostics](#diagnostics) - [ECS (Entity Component System)](#ecs-entity-component-system) + - [Embedded](#embedded) - [Games](#games) - [Gizmos](#gizmos) - [Helpers](#helpers) @@ -333,6 +334,12 @@ Example | Description [System Piping](../examples/ecs/system_piping.rs) | Pipe the output of one system into a second, allowing you to handle any errors gracefully [System Stepping](../examples/ecs/system_stepping.rs) | Demonstrate stepping through systems in order of execution. +## Embedded + +Example | Description +--- | --- +[`no_std` Compatible Library](../examples/no_std/library/src/lib.rs) | Example library compatible with `std` and `no_std` targets + ## Games Example | Description diff --git a/examples/no_std/README.md b/examples/no_std/README.md new file mode 100644 index 0000000000..9c57b5fd94 --- /dev/null +++ b/examples/no_std/README.md @@ -0,0 +1,18 @@ +# `no_std` Examples + +This folder contains examples for how to work with `no_std` targets and Bevy. +Refer to each example individually for details around how it works and what features you may need to enable/disable to allow a particular target to work. + +## What is `no_std`? + +`no_std` is a Rust term for software which doesn't rely on the standard library, [`std`](https://doc.rust-lang.org/stable/std/). +The typical use for `no_std` is in embedded software, where the device simply doesn't support the standard library. +For example, a [Raspberry Pi Pico](https://www.raspberrypi.com/documentation/microcontrollers/pico-series.html) has no operating system to support threads or filesystem operations. + +For these platforms, Rust has a more fundamental alternative to `std`, [`core`](https://doc.rust-lang.org/stable/core/). +A large portion of Rust's `std` actually just re-exports items from `core`, such as iterators, `Result`, and `Option`. + +In addition, `std` also re-exports from another crate, [`alloc`](https://doc.rust-lang.org/stable/alloc/). +This crate is similar to `core` in that it's generally available on all platforms. +Where it differs is that its inclusion requires access to a [global allocator](https://doc.rust-lang.org/stable/std/alloc/trait.GlobalAlloc.html). +Currently, Bevy relies heavily on allocation, so we consider `alloc` to be just as available, since without it, Bevy will not compile. diff --git a/examples/no_std/library/Cargo.toml b/examples/no_std/library/Cargo.toml new file mode 100644 index 0000000000..3cd07b55fb --- /dev/null +++ b/examples/no_std/library/Cargo.toml @@ -0,0 +1,45 @@ +[package] +name = "no_std_library" +version = "0.1.0" +edition = "2024" + +# Normally we'd put all dependencies in [dependencies], but this syntax is easier to document +[dependencies.bevy] +# In your library you'd use version = "x.y.z", but since this is an example inside the Bevy +# repository we use a path instead. +path = "../../../" +# Since `std` is a default feature, first we disable default features +default-features = false +# We're free to enable any features our library needs. +# Note that certain Bevy features rely on `std`. +features = [ + # "bevy_color", + # "bevy_state", +] + +[features] +# `no_std` is relatively niche, so we choose defaults to cater for the majority of our users. +default = ["std"] + +# Below are some features we recommend libraries expose to assist with their usage and their own testing. + +# Uses the Rust standard library. +std = ["bevy/std"] + +# Uses `libm` for floating point functions. +libm = ["bevy/libm"] + +# Rely on `critical-section` for synchronization primitives. +critical-section = ["bevy/critical-section"] + +# Enables access to browser APIs in a web context. +web = ["bevy/web"] + +[lints.clippy] +# These lints are very helpful when working on a library with `no_std` support. +# They will warn you if you import from `std` when you could've imported from `core` or `alloc` +# instead. +# Since `core` and `alloc` are available on any target that has `std`, there is no downside to this. +std_instead_of_core = "warn" +std_instead_of_alloc = "warn" +alloc_instead_of_core = "warn" diff --git a/examples/no_std/library/README.md b/examples/no_std/library/README.md new file mode 100644 index 0000000000..a82bc85691 --- /dev/null +++ b/examples/no_std/library/README.md @@ -0,0 +1,64 @@ +# Bevy `no_std` Compatible Library + +This example demonstrates how to create a `no_std`-compatible library crate for use with Bevy. +For the sake of demonstration, this library adds a way for a component to be added to an entity after a certain delay has elapsed. +Check the [Cargo.toml](Cargo.toml) and [lib.rs](src/lib.rs) for details around how this is implemented, and how we're able to make a library compatible for all users in the Bevy community. + +## Testing `no_std` Compatibility + +To check if your library is `no_std` compatible, it's not enough to just compile with your `std` feature disabled. +The problem is dependencies can still include `std` even if the top-most crate is declared as `#![no_std]`. +Instead, you need to compile your library without the standard library at all. + +The simplest way to compile Rust code while ensuring `std` isn't linked is to simply use a target without the standard library. +Targets with [Tier 2](https://doc.rust-lang.org/beta/rustc/platform-support.html#tier-2-without-host-tools) or [Tier 3](https://doc.rust-lang.org/beta/rustc/platform-support.html#tier-3) support often do not have access to `std`, and therefore can _only_ compile if `no_std` compatible. + +Some recommended targets you can check against are: + +* [`x86_64-unknown-none`](https://doc.rust-lang.org/beta/rustc/platform-support/x86_64-unknown-none.html) + * Representative of desktop architectures. + * Should be the most similar to typical `std` targets so it's a good starting point when porting existing libraries. +* [`wasm32v1-none`](https://doc.rust-lang.org/beta/rustc/platform-support/wasm32v1-none.html) + * Newer WebAssembly target with the bare minimum functionality for broad compatibility. + * Similar to `wasm32-unknown-unknown`, which is typically used for web builds. +* [`thumbv6m-none-eabi`](https://doc.rust-lang.org/beta/rustc/platform-support/thumbv6m-none-eabi.html) + * Representative of embedded platforms. + * Has only partial support for atomics, making this target a good indicator for atomic incompatibility in your code. + +Note that the first time you attempt to compile for a new target, you will need to install the supporting components via `rustup`: + +```sh +rustup target add x86_64-unknown-none +``` + +Once installed, you can check your library by specifying the appropriate features and target: + +```sh +cargo check --no-default-features --features libm,critical-section --target x86_64-unknown-none +``` + +### CI + +Checking `no_std` compatibility can be tedious and easy to forget if you're not actively using it yourself. +To avoid accidentally breaking that compatibility, we recommend adding these checks to your CI pipeline. +For example, here is a [GitHub Action](https://github.com/features/actions) you could use as a starting point: + +```yml +jobs: + check-compiles-no-std: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + target: + - "x86_64-unknown-none" + - "wasm32v1-none" + - "thumbv6m-none-eabi" + steps: + - uses: actions/checkout@v4 + - uses: dtolnay/rust-toolchain@stable + with: + targets: ${{ matrix.target }} + - name: Check Compile + run: cargo check --no-default-features --features libm,critical-section --target ${{ matrix.target }} +``` diff --git a/examples/no_std/library/src/lib.rs b/examples/no_std/library/src/lib.rs new file mode 100644 index 0000000000..bb10363ce0 --- /dev/null +++ b/examples/no_std/library/src/lib.rs @@ -0,0 +1,137 @@ +//! Example `no_std` compatible Bevy library. + +// The first step to a `no_std` library is to add this annotation: + +#![no_std] + +// This does 2 things to your crate: +// 1. It prevents automatically linking the `std` crate with yours. +// 2. It switches to `core::prelude` instead of `std::prelude` for what is implicitly +// imported in all modules in your crate. + +// It is common to want to use `std` when it's available, and fall-back to an alternative +// implementation which may make compromises for the sake of compatibility. +// To do this, you can conditionally re-include the standard library: + +#[cfg(feature = "std")] +extern crate std; + +// This still uses the `core` prelude, so items such as `std::println` aren't implicitly included +// in all your modules, but it does make them available to import. + +// Because Bevy requires access to an allocator anyway, you are free to include `alloc` regardless +// of what features are enabled. +// This gives you access to `Vec`, `String`, `Box`, and many other allocation primitives. + +extern crate alloc; + +// Here's our first example of using something from `core` instead of `std`. +// Since `std` re-exports `core` items, they are the same type just with a different name. +// This means any 3rd party code written for `std::time::Duration` will work identically for +// `core::time::Duration`. + +use core::time::Duration; + +// With the above boilerplate out of the way, everything below should look very familiar to those +// who have worked with Bevy before. + +use bevy::prelude::*; + +// While this example doesn't need it, a lot of fundamental types which are exclusively in `std` +// have alternatives in `bevy::platform_support`. +// If you find yourself needing a `HashMap`, `RwLock`, or `Instant`, check there first! + +#[expect(unused_imports, reason = "demonstrating some available items")] +use bevy::platform_support::{ + collections::{HashMap, HashSet}, + hash::DefaultHasher, + sync::{ + atomic::{AtomicBool, AtomicUsize}, + Arc, Barrier, LazyLock, Mutex, Once, OnceLock, RwLock, Weak, + }, + time::Instant, +}; + +// Note that `bevy::platform_support::sync::Arc` exists, despite `alloc::sync::Arc` being available. +// The reason is not every platform has full support for atomic operations, so `Arc`, `AtomicBool`, +// etc. aren't always available. +// You can test for their inclusion with `#[cfg(target_has_atomic = "ptr")]` and other related flags. +// You can get a more cross-platform alternative from `portable-atomic`, but Bevy handles this for you! +// Simply use `bevy::platform_support::sync` instead of `core::sync` and `alloc::sync` when possible, +// and Bevy will handle selecting the fallback from `portable-atomic` when it is required. + +/// Plugin for working with delayed components. +/// +/// You can delay the insertion of a component by using [`insert_delayed`](EntityCommandsExt::insert_delayed). +pub struct DelayedComponentPlugin; + +impl Plugin for DelayedComponentPlugin { + fn build(&self, app: &mut App) { + app.register_type::() + .add_systems(Update, tick_timers); + } +} + +/// Extension trait providing [`insert_delayed`](EntityCommandsExt::insert_delayed). +pub trait EntityCommandsExt { + /// Insert the provided [`Bundle`] `B` with a provided `delay`. + fn insert_delayed(&mut self, bundle: B, delay: Duration) -> &mut Self; +} + +impl EntityCommandsExt for EntityCommands<'_> { + fn insert_delayed(&mut self, bundle: B, delay: Duration) -> &mut Self { + self.insert(( + DelayedComponentTimer(Timer::new(delay, TimerMode::Once)), + DelayedComponent(bundle), + )) + .observe(unwrap::) + } +} + +impl EntityCommandsExt for EntityWorldMut<'_> { + fn insert_delayed(&mut self, bundle: B, delay: Duration) -> &mut Self { + self.insert(( + DelayedComponentTimer(Timer::new(delay, TimerMode::Once)), + DelayedComponent(bundle), + )) + .observe(unwrap::) + } +} + +#[derive(Component, Deref, DerefMut, Reflect, Debug)] +#[reflect(Component)] +struct DelayedComponentTimer(Timer); + +#[derive(Component)] +#[component(immutable)] +struct DelayedComponent(B); + +#[derive(Event)] +struct Unwrap; + +fn tick_timers( + mut commands: Commands, + mut query: Query<(Entity, &mut DelayedComponentTimer)>, + time: Res