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:
parent
4fca331bb6
commit
958c9bb652
25
.github/workflows/ci.yml
vendored
25
.github/workflows/ci.yml
vendored
@ -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
|
||||
|
14
Cargo.toml
14
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
|
||||
|
@ -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)]
|
||||
|
@ -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
18
examples/no_std/README.md
Normal 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.
|
45
examples/no_std/library/Cargo.toml
Normal file
45
examples/no_std/library/Cargo.toml
Normal 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"
|
64
examples/no_std/library/README.md
Normal file
64
examples/no_std/library/README.md
Normal 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 }}
|
||||
```
|
137
examples/no_std/library/src/lib.rs
Normal file
137
examples/no_std/library/src/lib.rs
Normal 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());
|
||||
}
|
Loading…
Reference in New Issue
Block a user