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

  1. Bare-metal firmware and embedded microcontrollers (thumbv7em-none-eabihf, riscv32imac-unknown-none-elf).
  2. Operating system kernels, hypervisors, and bootloaders (where your Rust code is the operating system).
  3. 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).