What the attribute does
Rust ships with layered libraries. At the bottom is core, which the Rust documentation calls the dependency-free foundation of the standard library: it links to no upstream libraries, no system libraries and no libc. On top sits alloc, with heap types such as Box and Vec. At the top is std, which adds threads, files, sockets, processes and the other services you expect from a general-purpose operating system.
Putting #![no_std] at the root of a crate tells the compiler not to link std automatically, and to use the core prelude instead of the standard one. The Rust Reference names two reasons to do it: the crate targets a platform that does not support the standard library, or it deliberately does without the standard library’s capabilities, mainly dynamic memory allocation and file and network access.
In bare-metal embedded work, the first reason applies. On a microcontroller with no operating system, there is nothing for std to call. The Embedonomicon, the Rust embedded working group’s guide to bare-metal Rust, puts it plainly: a #![no_std] application can be the first or the only code that runs on a system, which makes it suitable for firmware, bootloaders and kernels.
What you keep and what you lose
core still gives you the language: primitive types, floats, strings, slices, iterators, Option and Result, and access to processor features such as atomic operations and SIMD instructions. What it lacks is anything that needs platform integration: heap allocation, I/O and concurrency.
The Embedded Rust Book summarises the difference:
| Feature | no_std | std |
|---|---|---|
| Heap (dynamic memory) | Only with alloc and an allocator | Yes |
Collections such as Vec and BTreeMap | Only with a global allocator | Yes |
| Stack overflow protection | No | Yes |
Runs initialisation code before main | No | Yes |
| Writing firmware, kernel or bootloader code | Yes | No |
The book adds that HashMap and HashSet are not available in a no_std environment because there is no secure random number generator to seed them. That is a reminder of something easy to forget: on bare metal, randomness is your job too.
What a no_std program has to provide
Leaving out std also leaves out its runtime. std sets up stack overflow protection, processes command-line arguments and spawns the main thread before main runs. A no_std application must set up whatever it needs itself. In practice that means a few pieces:
- A panic handler.
coreexpects the program to define what happens on a panic. The Rust Reference requires exactly one function marked#[panic_handler]in the whole dependency graph, with a signature that never returns. The Reference’s own example logs the panic message and then halts. - An entry point. The Embedonomicon’s smallest program uses
#![no_main], because Rust’s usualmainassumes things such as command-line arguments. A runtime crate for the target, such ascortex-m-rtfor Arm Cortex-M parts, supplies the real entry point and startup code. - A memory layout. The linker has to know where flash and RAM are. The Embedded Rust Book’s Cortex-M template keeps this in a
memory.xfile that lists the part’s flash and RAM. - An allocator, if you want one. To use
alloc, declare a global allocator. The Embedded Rust Book also points to fixed-capacity collections in the heapless crate as an alternative.
You also build for a target triple that has no operating system in its name, such as thumbv7em-none-eabihf. Rust’s platform support list marks the targets that only support no_std development.
Libraries that work both ways
A crate can be useful on both a desktop and a device. The Cargo Book’s advice is to make no_std the base and add a std feature that turns the standard library back on, rather than a no_std feature that takes it away, because Cargo features should be additive. A user on bare metal then installs the crate with default features off.
Offline Protocol’s constrained-device crate shows the pattern from the user’s side. Its documentation, for Rust 1.87 or newer, installs it like this:
cargo add offline-protocol-leaf@=0.27.0 --no-default-features --features bare-metal-rng
The docs say not to combine bare-metal-rng with the default std feature, and to register the device’s hardware entropy source with the register_custom_getrandom macro from the getrandom crate, because enabling the feature alone does not supply randomness. A missing or predictable source makes key generation unusable or unsafe, the same issue that keeps HashMap out of a no_std build. For what else encrypted messaging needs on small hardware, see running encrypted mesh messaging on a microcontroller.
Where no_std ends
no_std is a property of a crate, not of a chip. Some embedded frameworks provide enough of an operating system for std, and the same platform list includes embedded targets with the full standard library alongside bare-metal targets for the same processors. The ESP32 family is one case where both kinds of target exist. Choose no_std when there is no operating system, when you need full control of memory and startup, or when you are writing a library that should run anywhere.