| name | rust-impl-no-std |
| description | Use when the user adds `#![no_std]`, supports embedded targets, needs the alloc crate, writes a `#[panic_handler]`, registers a `#[global_allocator]`, or designs a hybrid std + no_std library via feature flag. Prevents using std-only APIs in no_std, missing `#[panic_handler]`, picking alloc-free patterns when alloc is acceptable, and forgetting the `default = ["std"]` feature pattern. Covers: `#![no_std]` crate attribute, core crate (no allocation, no OS), alloc crate (Vec/Box/Rc/Arc/String when allocator available), `extern crate alloc;`, `#[panic_handler]` requirement when not std, `#[global_allocator]` hook, lang items (when needed in embedded), default = ["std"] feature pattern for hybrid library, testing no_std crates. Keywords: no_std, "#![no_std]", "no-std", embedded, core, alloc, "extern crate alloc", "panic_handler", "global_allocator", "lang items", "default = [std]", hybrid std, "no_std library", "no allocator", "embassy", "embedded Rust", "thumb target", "Cortex-M", "panic = abort", "core::fmt".
|
| license | MIT |
| compatibility | Designed for Claude Code. Requires Rust 1.85+, edition 2024. |
| metadata | {"author":"OpenAEC-Foundation","version":"1.0"} |
rust-impl-no-std
The no_std Rust skill: how to opt out of the standard library with #![no_std], work against core only, pull in alloc when a heap is available, register a #[panic_handler] and optional #[global_allocator], and design a hybrid library that builds on both std and no_std consumers via a default = ["std"] feature.
Cross-references: [[rust-core-stdlib-overview]] (what std contains that core does not), [[rust-impl-cargo-project]] (features, profiles, panic = "abort"), [[rust-syntax-unsafe]] (#[global_allocator] involves the unsafe GlobalAlloc trait), [[rust-impl-cross-compile]] (thumbv7em-none-eabihf and other bare-metal targets).
When to use this skill
- User adds
#![no_std] to lib.rs / main.rs and the compiler starts complaining about missing types.
- User targets an embedded board (Cortex-M, RISC-V MCU) and asks how to structure the crate.
- User wants
Vec / Box / String in a no_std crate and is unsure whether alloc is allowed.
- User writes their first
#[panic_handler] or hits the "duplicate panic handler" link error.
- User wants to plug in a custom allocator via
#[global_allocator].
- User has a library that some users will consume from std applications and others from no_std firmware, and asks how to support both.
- User asks why
std::format! doesn't compile in their crate.
- User asks why
HashMap cannot be imported from alloc.
For multi-target build matrices and target triples see [[rust-impl-cross-compile]]. For unsafe-trait constraints on GlobalAlloc see [[rust-syntax-unsafe]].
Decision tree: do I really need no_std?
Will this code ever run on a target WITHOUT an operating system?
NO -> Keep std. Done.
YES -> Continue.
Does the target have a heap allocator available?
YES (embedded with embedded-alloc, OS kernel with own allocator, WASM)
-> #![no_std] + extern crate alloc;
NO (bare-metal with strictly bounded memory)
-> #![no_std] alone, no alloc. Use heapless / static buffers.
Is this a library that std users ALSO need to consume?
YES -> Hybrid pattern: default = ["std"], std = [].
Code is no_std by default, `std` feature re-enables std-only APIs.
NO -> Pure no_std crate. Don't add std as a feature.
ALWAYS choose pure no_std over the hybrid pattern when the crate has no realistic std consumer: the hybrid pattern doubles testing matrix and cfg complexity. NEVER add #![no_std] to a binary crate "just in case" without an actual no_std target.
Anatomy of a #![no_std] crate
#![no_std]
#![cfg_attr(not(test), no_main)]
extern crate alloc;
use core::fmt::Write;
use alloc::vec::Vec;
use alloc::string::String;
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
#[global_allocator]
static GLOBAL: MyAllocator = MyAllocator;
Source: Rust Reference, names/preludes
What changes versus a std crate:
| Concern | Without no_std (std) | With #![no_std] |
|---|
| Prelude | std::prelude::rust_2024 | core::prelude::rust_2024 |
| Heap types | std::vec::Vec, std::string::String | alloc::vec::Vec, alloc::string::String (needs extern crate alloc;) |
| Formatting | std::fmt, std::format! | core::fmt, core::write!, core::writeln! (no format! without alloc) |
| Panic | runtime-provided | user MUST provide #[panic_handler] (binary / cdylib / dylib) |
| Entry point | fn main() | platform-specific (e.g. cortex-m-rt::entry) plus #![no_main] |
println! | yes | NO (no stdout); use semihosting, defmt, or a serial logger |
ALWAYS reach for core:: first when writing no_std code. Only escalate to alloc:: when the algorithm needs the heap, and only escalate further (std::) inside #[cfg(feature = "std")] blocks.
NEVER write extern crate std; in a no_std crate. NEVER call std::format!, std::println!, std::eprintln!, or anything under std::fs / std::net / std::process from no_std code.
core vs alloc vs std: what lives where
core = language minimum. Always available. No heap, no OS.
alloc = adds heap-backed types. Requires a global allocator.
std = adds OS abstractions (threads, files, network, time, env, process).
std re-exports everything from core and alloc, plus its own additions.
Source: alloc crate documentation
core contains (selected, no allocation needed):
core::option::Option, core::result::Result (re-exported via prelude).
core::iter::Iterator, IntoIterator, adapters (map, filter, take).
core::fmt::{Debug, Display, Write, Formatter} and the format-string machinery used by write!.
core::mem (size_of, align_of, swap, replace, take, ManuallyDrop).
core::ptr (raw pointer ops, NonNull, addr_of!, addr_of_mut!).
core::cell::{Cell, RefCell, OnceCell, UnsafeCell}.
core::sync::atomic::* (AtomicUsize, AtomicBool, Ordering, ...).
core::marker::{PhantomData, Send, Sync, Sized, Copy}.
core::error::Error (stable in no_std since 1.81).
core::panic::PanicInfo for #[panic_handler].
alloc adds (heap-backed types, requires an allocator):
alloc::boxed::Box, alloc::vec::Vec, alloc::string::String.
alloc::rc::{Rc, Weak}, alloc::sync::{Arc, Weak}.
alloc::collections::{BTreeMap, BTreeSet, VecDeque, BinaryHeap, LinkedList}.
alloc::format! macro (heap-allocating formatting).
alloc::borrow::Cow, alloc::borrow::ToOwned.
alloc::slice::SliceConcatExt extras.
std adds (OS-bound):
std::collections::HashMap / HashSet (use hashbrown in no_std crates instead).
std::fs, std::io, std::net, std::process, std::thread, std::time::SystemTime, std::env.
std::sync::{Mutex, RwLock, Once, OnceLock, LazyLock} (these are NOT in core::sync; core::sync is atomics only).
ALWAYS check the docs.rs page for a crate before depending on it from no_std. Look for a default-features = false example and a no_std feature.
NEVER assume std::collections::HashMap has an alloc-only counterpart: it does not. Use hashbrown::HashMap (the same backing implementation std uses) with default-features = false.
#[panic_handler]: required signature
"must appear once in the dependency graph of a binary / dylib / cdylib crate"
Source: Rustonomicon, panic-handler
The exact required signature, verified against the Rustonomicon:
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
| Constraint | Rule |
|---|
| Argument | exactly one: &core::panic::PanicInfo |
| Return type | ! (the never type); function must diverge |
| Visibility | private (fn, no pub) |
| Frequency | exactly ONE in the entire dependency graph of the final artefact |
| Location | typically the binary crate; libraries SHOULD NOT define one |
Production-grade handlers:
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
cortex_m::asm::udf()
}
extern crate panic_halt;
ALWAYS provide the panic handler in the BINARY crate, never inside a reusable library. NEVER write panic!() inside a #[panic_handler]: re-entry into the panic machinery is undefined.
NEVER copy the handler into multiple library crates; the linker fails with "duplicate lang item panic_impl" (error E0152) when more than one is found.
#[global_allocator]: hooking the heap
Once extern crate alloc; is in scope, Box, Vec, String, and friends call into a single global allocator. For std targets that allocator is provided automatically. In no_std you provide it yourself.
use embedded_alloc::Heap;
#[global_allocator]
static HEAP: Heap = Heap::empty();
fn init_heap() {
use core::mem::MaybeUninit;
const HEAP_SIZE: usize = 1024;
static mut MEMORY: [MaybeUninit<u8>; HEAP_SIZE] = [MaybeUninit::uninit(); HEAP_SIZE];
unsafe { HEAP.init(MEMORY.as_ptr() as usize, HEAP_SIZE) }
}
Source: Rust Embedded Book
| Rule | Detail |
|---|
| Trait implemented | core::alloc::GlobalAlloc (this is an unsafe trait) |
| Frequency | exactly ONE #[global_allocator] static per final artefact |
| Lifetime | 'static; usually a static variable |
| Initialization | allocator may need a runtime init() call BEFORE the first allocation |
ALWAYS call the allocator's init routine before the first Box::new / Vec::push / heap-allocating call. NEVER allocate from inside #[panic_handler] or other interrupt-context code unless the allocator documents it as safe.
NEVER expose #[global_allocator] from a library crate: it is a global, application-level choice. Libraries depend on alloc, applications pick the allocator.
Hybrid std + no_std library pattern
The canonical Cargo recipe, verified against the Cargo features reference:
[package]
name = "mylib"
edition = "2024"
rust-version = "1.85"
[features]
default = ["std"]
std = ["alloc", "serde?/std"]
alloc = []
[dependencies]
serde = { version = "1", optional = true, default-features = false }
#![cfg_attr(not(feature = "std"), no_std)]
#[cfg(feature = "alloc")]
extern crate alloc;
#[cfg(feature = "alloc")]
use alloc::string::String;
#[cfg(feature = "std")]
use std::io;
Three feature tiers, in dependency order:
(none) -> pure core. No heap, no OS. Most restrictive.
alloc -> core + heap. Vec, Box, String, BTreeMap available.
std -> core + alloc + OS. Files, network, threads, HashMap available.
ALWAYS gate extern crate alloc; behind #[cfg(feature = "alloc")] when the crate also supports pure-core builds. NEVER make std the only opt-in: by following the additive-features principle, the default must include everything most users want, and the absence of a feature must REMOVE functionality, not break compilation for the default case.
NEVER write default = [] for a hybrid library: every existing std consumer's build would break the moment they upgrade.
Testing a #[no_std] crate
cargo test defaults to building a test harness that depends on std. Three paths:
-
Std-only tests : tests use std, library still compiles for no_std targets.
#[cfg(test)]
mod tests {
use super::*;
}
Verify the no_std path separately:
cargo build --no-default-features --target thumbv7em-none-eabihf
-
Integration tests in a separate std crate: workspace member with [dev-dependencies] mylib = { path = "..", default-features = false }.
-
defmt-test or embedded-test for on-target testing (out of scope for this skill; see embedded-book chapter on testing).
ALWAYS run cargo build --no-default-features for every published no_std crate as part of CI, and ALSO cargo build --no-default-features --features alloc if the hybrid pattern is used. NEVER assume cargo test proves the no_std path: it does not.
Cargo profile: pair no_std with panic = "abort"
"panic = "abort" ... Required for some no_std targets."
Source: vooronderzoek §19, verified against Cargo profiles reference
[profile.release]
panic = "abort"
lto = "fat"
codegen-units = 1
opt-level = "z"
strip = "symbols"
[profile.dev]
panic = "abort"
| Setting | Reason for no_std |
|---|
panic = "abort" | unwinding requires eh_personality lang item; abort skips it entirely |
opt-level = "z" or "s" | embedded flash is small; size beats speed |
lto = "fat" | cross-crate inlining removes unused std-shim code |
strip = "symbols" | smaller binary; debug info goes to a separate .elf |
ALWAYS set panic = "abort" for both [profile.dev] AND [profile.release] in pure embedded binaries: tests would otherwise mismatch.
NEVER mix panic = "unwind" with no_std targets that lack eh_personality: link fails with "undefined reference to rust_eh_personality".
Quick recipes
Add the alloc crate to an existing no_std lib
#![no_std]
extern crate alloc;
use alloc::vec::Vec;
use alloc::string::String;
A minimal Cortex-M binary, no allocator
#![no_std]
#![no_main]
use core::panic::PanicInfo;
use cortex_m_rt::entry;
#[entry]
fn main() -> ! {
loop {}
}
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
cortex_m::asm::udf()
}
Replace format! with core::write! to a heapless buffer
use core::fmt::Write;
use heapless::String;
let mut s: String<32> = String::new();
write!(s, "value = {}", 42).unwrap();
Conditional std::error::Error impl
#![cfg_attr(not(feature = "std"), no_std)]
impl core::error::Error for MyError {}
Validation checklist
Before publishing or merging:
cargo build --no-default-features succeeds (proves the pure-core / pure-no_std path).
cargo build --no-default-features --features alloc succeeds when the alloc tier exists.
cargo build succeeds (proves the default std path still works for std consumers).
- The binary crate has exactly ONE
#[panic_handler] in the final dependency graph.
- The binary crate has at most ONE
#[global_allocator] in the final dependency graph.
- No
use std::... lines exist outside #[cfg(feature = "std")] blocks.
cargo build --target <embedded-triple> succeeds when an embedded target is supported.
- Profile has
panic = "abort" if the target requires it.
core::error::Error (not std::error::Error) is used for error trait impls in no_std-friendly code.
Reference links
- [[references/methods.md]] : full inventory of what lives in
core vs alloc vs std, complete #[panic_handler] + #[global_allocator] signatures, every relevant #[cfg] predicate, the no_std prelude per edition.
- [[references/examples.md]] : full crate templates : minimal Cortex-M binary, no_std library with alloc, hybrid std + no_std library with three feature tiers, custom allocator implementation, heapless string formatting.
- [[references/anti-patterns.md]] : seven concrete anti-patterns with diagnostics, error codes (E0152, undefined
rust_eh_personality, missing alloc import), and the exact fix per case.