Low-Level Systems Programming
no_std Fundamentals
Adding the crate-level #![no_std] attribute at the top of src/lib.rs or src/main.rs instructs rustc not to link the operating-system-dependent standard library (std), linking only the platform-agnostic core library instead.
+-------------------------------------------------------------------+
| `std` |
| OS Threads (`std::thread`), File System (`std::fs`), |
| Sockets (`std::net`), Process/Env (`std::process`, `std::env`) |
| +-------------------------------------------------------------+ |
| | `alloc` | |
| | Heap Containers (`Box`, `Vec`, `String`, `Arc`, `BTreeMap`)| |
| | +-------------------------------------------------------+ | |
| | | `core` | | |
| | | Primitives, Slices, `Option`, `Result`, `Iterator`, | | |
| | | Trait System, `Pin`/`Future`, `Atomic*`, `CStr` | | |
| | | (Zero OS dependencies! Zero heap allocation!) | | |
| | +-------------------------------------------------------+ | |
| +-------------------------------------------------------------+ |
+-------------------------------------------------------------------+
When to Write #![no_std] Code
- Bare-metal firmware and embedded microcontrollers (
thumbv7em-none-eabihf,riscv32imac-unknown-none-elf). - Operating system kernels, hypervisors, and bootloaders (where your Rust code is the operating system).
- Minimal WebAssembly modules (
wasm32-unknown-unknown) and cryptographic/parsing libraries that want to be usable across both server applications and embedded devices.
Writing a Dual-Mode Library (no_std by Default, std Optional)
General-purpose crates (such as serde, bytes, crc32fast, and bitflags) use #![cfg_attr(not(feature = "std"), no_std)] so the crate is #![no_std] when compiled with --no-default-features, while still integrating with std::io when the "std" feature is enabled.
Note that since Rust 1.81, the core::error::Error trait is available directly in core! A #![no_std] crate no longer needs std just to implement the Error trait on its custom error types.
// In a library crate, this keeps the crate `#![no_std]` whenever the `std` feature is disabled,
// while allowing it to run in normal hosted `std` tests and binaries:
#![cfg_attr(not(feature = "std"), no_std)]
use core::error::Error;
use core::fmt;
#[derive(Debug, PartialEq, Eq)]
pub struct BufferFullError {
pub capacity: usize,
}
impl fmt::Display for BufferFullError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "fixed buffer of capacity {} is full", self.capacity)
}
}
// `core::error::Error` works in `#![no_std]` (Rust 1.81+ / 2024 edition)!
impl Error for BufferFullError {}
/// A stack-allocated fixed-capacity vector usable in `#![no_std]` without an allocator.
pub struct FixedVec<T, const N: usize> {
items: [Option<T>; N],
len: usize,
}
impl<T: Copy, const N: usize> FixedVec<T, N> {
pub const fn new() -> Self {
Self {
items: [None; N],
len: 0,
}
}
pub fn try_push(&mut self, item: T) -> Result<(), BufferFullError> {
if self.len >= N {
return Err(BufferFullError { capacity: N });
}
self.items[self.len] = Some(item);
self.len += 1;
Ok(())
}
pub const fn len(&self) -> usize {
self.len
}
pub const fn is_empty(&self) -> bool {
self.len == 0
}
}
fn main() {
let mut buf = FixedVec::<u16, 2>::new();
buf.try_push(100).unwrap();
buf.try_push(200).unwrap();
let overflow = buf.try_push(300).unwrap_err();
println!("FixedVec len={}, overflow error: {}", buf.len(), overflow);
}
FixedVec len=2, overflow error: fixed buffer of capacity 2 is full
#![no_std] Library vs #![no_std] Binary
| Crate Kind | What #![no_std] Requires |
|---|---|
#![no_std] Library (src/lib.rs) |
Nothing extra! Compiles out of the box using core::* items, and can still use std inside #[cfg(test)] unit tests and tests/ integration tests. |
#![no_std] Bare-Metal Binary (src/main.rs) |
Also requires #![no_main], a #[panic_handler] function (fn(&PanicInfo) -> !), and a target linker script defining the hardware entry symbol (such as _start or Reset). |