#![no_std] indicates that the crate does not automatically link to std and uses the core prelude. It does not mean "no operating system," nor does it mean "cannot allocate memory": a no_std library can be used by standard Linux programs or in bare-metal environments that provide appropriate runtime support.
The difficulty of kernel modules lies not just in removing std. They must adhere to the kernel's resource lifecycle, allocation contexts, locking rules, and build configurations; simply renaming user-space APIs does not yield a correct module.
No heap dependency; some atomic capabilities are target-limited
alloc
Box, Vec, String, Rc, Arc (on suitable targets), BTreeMap
Requires allocation support from the final program
std
Platform interfaces like files, network, threads, and re-exports of core/alloc
Target platform must have corresponding implementations
HashMap is not in alloc; you need to choose additional implementations based on the target or use other collections. format_args! constructs formatting arguments and does not require allocating a String; only alloc::format! allocates a string. Providing core::fmt::Write allows writing formatted output to a fixed buffer or device, which does not imply that heap allocation must be implemented first. alloc documentation
no_std is also not a checker that "guarantees the dependency graph has absolutely no std." A dependency's default features might still introduce std; you need to check the target and feature combinations. For targets where std is fundamentally unavailable, relevant dependencies will be exposed during the build phase. Rust crate rules
Which dependency layer does no_std remove?
Start with slice parsing and add requirements one at a time. Fixed-buffer formatting can stay in core; Vec introduces allocation responsibility; std::fs requires platform APIs. These are distinct dependency choices: no_std alone does not determine heap availability, operating system presence or program entry.
Preparing the visual
Which dependency layer does no_std remove?
A no_std core can use fixed buffers; Vec needs alloc and allocation support, while files need std or a target adapter.
Save this as nostd.rs and compile with rustc --edition=2024 --crate-type=rlib nostd.rs. This builds a library linkable by other programs, without needing to define _start, panic_handler, or link scripts. It can be called by a standard std test program to verify empty input, short input, and correct big-endian behavior.
This separation has practical value: protocol parsing, state machines, and data processing should be placed in testable core libraries, while device access, scheduling, and allocation strategies belong in the target adaptation layer. no_std is one way to enforce this dependency boundary.
Verifying no_std Core on an std Host
Compile the previous file as a library named parser, and create the following check.rs separately. The library still does not depend on std, but the test entry point uses the host runtime environment.
The last assertion clarifies that this interface only reads the first two bytes, allowing additional input afterwards; if the protocol requires packets to be exactly two bytes, length should be checked separately at the entry point. Host execution verifies parsing contracts but cannot replace checks for alignment, atomic capabilities, linking, and hardware behavior on the target.
Fixed Buffers Can Also Use Formatting Interfaces
The following, saved separately as fixed.rs, is a library that uses absolutely no alloc. The buffer only accepts complete UTF-8 fragments; if space is insufficient, the current write_str call leaves the value unchanged. Fields are private, preventing callers from forging len or writing invalid encodings into already-used regions.
Similar to parser, first compile the library with --crate-name fixed --crate-type=rlib, then link the host program via --extern fixed=libfixed.rlib. TwoParts intentionally writes in two parts to demonstrate that "one write_str failure leaves everything unchanged" does not equal "the entire write! failure leaves everything unchanged": the first fragment was already written, and the second caused an overflow.
replace_fmt first writes to a temporary fixed buffer and only replaces the current value upon success, providing a commit boundary for the entire output; the cost is temporarily occupying N bytes of storage. No heap allocation does not mean memory usage can be ignored; large local objects with large N will still squeeze stack space. Here, fmt::Error only indicates that formatting cannot continue; if device writing needs to carry detailed I/O errors, additional error saving or interfaces should be designed. core::fmt::Write
alloc is Optional, but Final Allocation Responsibility Does Not Disappear
A no_std library that uses allocation can be written like this:
#![no_std]externcrate alloc;usealloc::vec::Vec;pubfncopy_bytes(input:&[u8])->Result<Vec<u8>, alloc::collections::TryReserveError>{letmut out =Vec::new();
out.try_reserve_exact(input.len())?;
out.extend_from_slice(input);Ok(out)}
After successfully reserving space, copying u8 does not require further expansion. This function passes capacity/allocation failures to the caller via Result, but it does not prove that all allocations in the entire program are recoverable. The final executable environment still needs to satisfy the global allocation and other link conditions required by alloc; standard std programs usually already provide this.
Bare-metal custom allocators must also satisfy conventions regarding alignment, layout matching, concurrency, and interrupt contexts. You cannot treat a bump pointer example as a GlobalAlloc implementation ready for use in any multi-threaded program.
no_main, panic, and Hardware Startup Are Another Set of Decisions
#![no_main] disables the usual main entry arrangement and is not automatically implied by no_std. Bare-metal executables usually require target startup code, initial stack, link layout, data initialization, and interrupt vectors; these depend on the chip and runtime framework and cannot be represented by an empty loop for all startup work.
Final no-std programs usually require a panic handler provided from somewhere, and having each library define its own leads to duplication. A panic handler can log, reset, or halt depending on the platform; it is not inherently unable to output; whether unwinding is supported is determined by the target, runtime, and build strategy. Embedded Rust's no_std
Similarly, a busy loop does not equal low-power waiting; whether interrupts are enabled or the watchdog resets can affect its actual behavior. These belong to the target runtime environment and should be clarified in platform tutorials.
Rust-for-Linux: Using Target Kernel Source as Interface
Linux 6.1 merged initial Rust support, but this does not mean all subsequent architectures, configurations, or distributions can directly build arbitrary Rust modules. The kernel Rust API continues to evolve; code like kernel::file::Operations from old blogs cannot be treated as compilable templates for all versions.
Use Linux v6.18's rust_minimal as a starting point for reading: module! declares module metadata, Module::init returns module status, and Drop handles cleanup on exit; the example uses KVec and explicitly passes GFP_KERNEL on push, propagating failures with ?. This reflects actual interfaces better than piecing together user-space Vec, file operation traits, and fictional signatures. v6.18 minimal module
When learning, first verify dependencies in the target kernel source tree, then build the sample included in that version:
# Execute within the prepared target kernel source treemake LLVM=1 rustavailable
Subsequently, enable Rust and corresponding samples according to the tree's configuration dependencies, and complete the build using the kernel build system. The rustavailable check tool supports verification but does not replace configuration and actual compilation; requirements for Rust, bindgen, and LLVM should be read from the target version documentation, rather than fixed as "just install any latest version." Kernel Rust Quick Start
Ownership in the Kernel is Also Constrained by Execution Context
User-space Intuition
Questions to Ask in the Kernel
Use after successful allocation
Is sleeping allowed at the current location? How to return on allocation failure?
Just lock with Mutex
Are you currently in an interrupt/atomic context? How is lock ordering guaranteed?
Drop automatically cleans up
Is the object still referenced by callbacks, workqueues, or device access? Can cleanup block?
One reference count is enough
How to coordinate device removal, module unloading, and asynchronous execution?
The kernel provides its own allocation, synchronization, and error abstractions; you cannot directly replace them with std::sync::Mutex or user-space thread models. unsafe encapsulations must incorporate these additional conditions into the contract; the borrow checker does not automatically understand that hardware is still performing DMA or that a C callback has not yet finished.
Kernel panic behavior cannot be generalized as "always BUG() and stop the entire kernel." Implementations vary by version and configuration; the impact after an exception depends on the specific kernel path; you should check the target version's panic handler and runtime strategy. Regardless of the outcome, panic is not a substitute for handling ordinary, expected failures in the kernel. Kernel Rust Overview
Treat Initialization and Exit as Resource Dependencies
Consider a device object that first acquires resources, then registers callbacks, and finally allows workqueues to use it. If initialization fails at the second step, resources acquired in the first step should be reclaimed by the established owner; if it fails after successful registration, unregistration is also required, and waiting for in-flight usage to end according to subsystem conventions is necessary. The key during exit is not mechanically calling a few free functions, but first stopping new access, then ending existing access, and finally releasing referenced state.
Borrowing and reference counts help express the lifetime of software objects, but DMA, hardware interrupts, and C-saved contexts do not automatically become Rust references. The "last access has ended" fact must be established by the target subsystem's shutdown, synchronization, and unregistration interfaces before allowing Drop to release. Some unregistrations wait or sleep, which again requires exit to occur in a context that allows blocking.
Check Result
Evidence Obtained
Not Yet Covered
no_std rlib compiles successfully
The library can compile under this target and feature set
Final entry point, allocator, and runtime environment
std host boundary tests pass
Logical behavior corresponding to test inputs
Target hardware, interrupts, and kernel context
rustavailable succeeds
Target source tree recognizes current Rust tool support
Required configuration, module linking, and loading
Module build completes
Selected source/config can generate module artifacts
Device interaction, unload races, and stress behavior
Recommended Learning Progression
First, write pure data processing as a no_std library that can be tested on the host; then integrate panic, logging, and allocation in the target runtime framework; finally, read existing device or subsystem samples in the target kernel version, understanding their lifecycle item by item. This distinguishes language issues, target runtime issues, and kernel interface issues, avoiding taking on three layers of unknown conditions at once.