Rust Libraries That Work With or Without std
A no_std-ready library is a library written against core. One file then serves both worlds.
One line, two worlds
This line sits at the top of the library. It removes std only while the std feature is off:
#![cfg_attr(not(feature = "std"), no_std)]The switchboard
Features live in Cargo.toml. std starts on. A bare-metal user asks for the defaults to be off:
// Cargo.toml
// [features]
// default = ["std"]
// std = []
// alloc = []Heap code behind a gate
This function builds a String, so it needs the heap. Its gate keeps it out of heap-free builds. The core-only average beside it needs no gate:
#[cfg(feature = "alloc")]
pub fn describe(readings: &[u32]) -> alloc::string::String {
match average(readings) {
Some(avg) => alloc::format!("average is {avg} over {} readings", readings.len()),
None => alloc::string::String::from("no readings yet"),
}
}Capacity in the type
No heap often means fixed room, chosen up front. The heapless crate writes the limit into the type. This Vec stores its items inside itself:
let mut readings: Vec<u32, 4> = Vec::new();
for value in [10, 20, 30, 40] {
readings.push(value).unwrap();
}
println!("len = {}", readings.len());Full is an answer, not a crash
The Vec above is now full. One more push cannot grow it. The value comes back in an Err instead:
match readings.push(50) {
Ok(()) => println!("50 fitted"),
Err(value) => println!("full: {value} handed back"),
}Example
// A tiny fixed-capacity log, written against core only.
// The same code could sit inside a no_std crate unchanged.
struct RingLog {
buf: [u32; 3],
next: usize,
count: usize,
}
impl RingLog {
fn new() -> Self {
RingLog { buf: [0; 3], next: 0, count: 0 }
}
fn push(&mut self, value: u32) {
self.buf[self.next] = value;
self.next = (self.next + 1) % self.buf.len();
if self.count < self.buf.len() {
self.count += 1;
}
}
fn oldest_first(&self) -> [u32; 3] {
let mut out = [0; 3];
let start = if self.count < self.buf.len() { 0 } else { self.next };
for i in 0..self.count {
out[i] = self.buf[(start + i) % self.buf.len()];
}
out
}
}
fn main() {
let mut log = RingLog::new();
for value in [10, 20, 30, 40, 50] {
log.push(value);
}
println!("kept = {:?}", log.oldest_first());
println!("count = {}", log.count);
}Key points
- The pattern has three parts.
- A std feature, on by default.
- One cfg_attr line that removes std when the feature is off.
- Gates on the parts that need more
- Gate heap code behind an alloc feature.
- Core-only code needs no gate at all.
- It compiles in every mode
- Fixed-capacity types store their limit in the type.
- heapless::Vec<u32, 4> holds four numbers, never five.
- A full push hands the value back in an Err
- Hardware drivers use the same trick.
- The embedded-hal crate defines traits for pins, I2C, SPI and timers.
- A driver written against the traits runs on any chip whose HAL implements them
- When does no_std matter?
- Kernels, bootloaders, firmware and WebAssembly.
- All of them start with no operating system underneath
- A library written against core runs with std and without it.
- A library must not choose for its user.
- Offer std as a default feature, never as a demand.
In this example, RingLog stores three readings in a plain array. It uses nothing but core. No Vec, no heap, no std. Five values go in. Each new value overwrites the oldest slot, so the ring keeps 30, 40 and 50. The main function here is only a test driver wearing std. The RingLog code itself could move into a no_std crate unchanged. That is the whole trick of this lesson. Keep the logic in core. Let each home bring its own shell.
Note
The final program decides what exists
Try it Yourself
Edit the code, then press Run.
Running is simulated · Rust Playground
Exercise
A library must run on laptops and on a chip with no heap. What is the usual shape?