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>
This commit is contained in:
Zachary Harrold 2025-03-18 11:45:25 +11:00 committed by GitHub
parent 4fca331bb6
commit 958c9bb652
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
8 changed files with 312 additions and 2 deletions

View File

@ -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

View File

@ -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

View File

@ -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)]

View File

@ -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

18
examples/no_std/README.md Normal file
View File

@ -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.

View File

@ -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"

View File

@ -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 }}
```

View File

@ -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::<DelayedComponentTimer>()
.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<B: Bundle>(&mut self, bundle: B, delay: Duration) -> &mut Self;
}
impl EntityCommandsExt for EntityCommands<'_> {
fn insert_delayed<B: Bundle>(&mut self, bundle: B, delay: Duration) -> &mut Self {
self.insert((
DelayedComponentTimer(Timer::new(delay, TimerMode::Once)),
DelayedComponent(bundle),
))
.observe(unwrap::<B>)
}
}
impl EntityCommandsExt for EntityWorldMut<'_> {
fn insert_delayed<B: Bundle>(&mut self, bundle: B, delay: Duration) -> &mut Self {
self.insert((
DelayedComponentTimer(Timer::new(delay, TimerMode::Once)),
DelayedComponent(bundle),
))
.observe(unwrap::<B>)
}
}
#[derive(Component, Deref, DerefMut, Reflect, Debug)]
#[reflect(Component)]
struct DelayedComponentTimer(Timer);
#[derive(Component)]
#[component(immutable)]
struct DelayedComponent<B: Bundle>(B);
#[derive(Event)]
struct Unwrap;
fn tick_timers(
mut commands: Commands,
mut query: Query<(Entity, &mut DelayedComponentTimer)>,
time: Res<Time>,
) {
for (entity, mut timer) in &mut query {
timer.tick(time.delta());
if timer.just_finished() {
commands
.entity(entity)
.remove::<DelayedComponentTimer>()
.trigger(Unwrap);
}
}
}
fn unwrap<B: Bundle>(trigger: Trigger<Unwrap>, world: &mut World) {
if let Ok(mut target) = world.get_entity_mut(trigger.target()) {
if let Some(DelayedComponent(bundle)) = target.take::<DelayedComponent<B>>() {
target.insert(bundle);
}
}
world.despawn(trigger.observer());
}