Rust
Completed
-
Section 1: Getting Started
-
Section 2: Core Syntax
-
Section 3: Ownership and Borrowing
-
Section 4: Structuring Data
-
Section 5: Collections and Error Handling
-
Section 6: Traits and Generics
-
Section 7: Concurrency
-
Section 8: Building for the Web
-
Section 9: Memory and Performance
-
Section 10: More Standard Library and Ecosystem
-
Section 11: Advanced Rust
-
Section 12: Rust for Systems and WebAssembly
-
Section 13: Tooling and Best Practices
-
Section 14: Data Structures and Algorithms in Rust
-
Section 15: Practical Projects
-
Section 16: Interview Practice
-
Section 17: std::collections In Depth
-
Section 18: std::io and std::fs In Depth
-
Section 19: std::net
-
Section 20: std::option and std::result In Depth
-
Section 21: std::iter In Depth
-
Section 22: std::sync In Depth
-
Section 23: std::string and std::str
-
Section 24: Cargo and Crates.io Ecosystem
-
Section 25: Popular Crates Ecosystem
-
Section 26: Rust Memory Model Deep Dive
-
Section 27: More Practice Exercises
-
Section 28: More Interview Practice
-
Section 29: Async Rust Deep Dive
-
Section 30: Tokio Ecosystem In Depth
-
Section 31: Error Handling Ecosystem Deep Dive
-
Section 32: Serde In Depth
-
Section 33: Web Development with Rust Deep Dive
-
Section 34: Database Access Ecosystem
-
Section 35: Rust for Embedded Systems Deep Dive
-
Section 36: Rust Macros In Depth
-
Section 37: Advanced Trait System
-
Section 38: Unsafe Rust In Depth
-
Section 39: Rust CLI Development
-
Section 40: Testing Ecosystem Deep Dive
-
Section 41: WebAssembly Deep Dive
-
Section 42: Rust Design Patterns
-
Section 43: More Data Structures in Rust
-
Section 44: Final Practice Projects
-
Section 45: Rust Performance Optimization
-
Section 46: Rust Ecosystem Tooling
-
Section 47: More Interview and Review
180: Working Without an Allocator (alloc-free Rust)
Most of the time, we treat the heap like an infinite playground. We throw Vecs and Strings around without a second thought because the allocator handles the messy parts. But when you're writing a bootloader, a kernel, or firmware for a tiny microcontroller, that luxury vanishes. You're in no_std territory.
Working without an allocator isn't just about removing a dependency; it's a fundamental shift in how you think about data ownership. You can't just "grow" a collection. Everything must have a known size at compile time, or you have to be very clever with lifetimes and slices. Let's build a simple packet parser for a simulated temperature sensor to see this in action.
Stripping back to no_std
First, we have to tell the compiler we aren't using the standard library. I'll start by adding the attribute at the top of my crate. We'll also need core, which is the subset of std that doesn't require an allocator.
#![no_std]
#![no_main]
use core::panic::PanicInfo;
#[panic_handler]
fn panic(_info: &PanicInfo) -> ! {
loop {}
}
Since we have no main function in the traditional OS sense, I've added a dummy panic handler. In a real embedded project, this would probably trigger a system reset or blink an LED. Now, let's define what we're parsing. Imagine our sensor sends a packet: a 4-byte ID, a 4-byte float for temperature, and a 1-byte status flag.
Designing the SensorReading struct
I want a struct that represents this reading. Normally, I might want to include a human-readable name for the sensor, but that's where the no_std constraint hits. I can't use String.
struct SensorReading {
id: u32,
temperature: f32,
status: u8,
}
impl SensorReading {
fn from_bytes(bytes: &[u8]) -> Option<&'static SensorReading> {
// I'll come back to this implementation in a second
None
}
}
The "Muscle Memory" Mistake
Here is where I usually trip up when switching back to no_std. I started writing the parser and instinctively tried to collect the bytes into a Vec to make slicing easier. I literally typed let data = bytes.to_vec(); before remembering that Vec lives in the alloc crate, and I haven't defined an allocator.
The compiler caught me immediately: cannot find type Vec in this scope. It's a jarring reminder that the heap is gone. I can't copy data into a dynamic container; I have to work with the bytes exactly where they sit in memory.
Parsing via Slices and Casting
To get this working, I'll use slices and the from_be_bytes method (big-endian), which is provided by the primitive types in core. This is the "correct" way to handle binary data without allocation: map the slice directly to the types you need.
impl SensorReading {
fn parse(bytes: &[u8]) -> Result {
if bytes.len() < 9 {
return Err("Packet too short");
}
// Use try_into to get fixed-size arrays from the slice
let id_bytes: [u8; 4] = bytes[0..4].try_into().map_err(|_| "ID parse failed")?;
let temp_bytes: [u8; 4] = bytes[4..8].try_into().map_err(|_| "Temp parse failed")?;
let status = bytes[8];
Ok(SensorReading {
id: u32::from_be_bytes(id_bytes),
temperature: f32::from_be_bytes(temp_bytes),
status,
})
}
}
Notice that SensorReading is now a simple stack-allocated struct. I'm returning a Result where the error is a &'static str. Since static strings are stored in the binary's read-only data section, they don't require an allocator. This pattern—using slices and static references—is your bread and butter in alloc-free Rust.
If I had needed a variable-length name for the sensor, I would have had to either use a fixed-size array (e.g., [u8; 16]) or store a reference to the original input buffer with a lifetime (e.g., &'a str). That second option is more efficient but forces you to keep the raw input buffer alive as long as the SensorReading exists.
📋 Practical Task
Implementing a CRC-8 Checksum for the Sensor Parser
In a real-world scenario, no_std packets usually include a checksum to ensure data wasn't corrupted. Your task is to extend the SensorReading example.
The Requirements:
- Modify the
parsefunction to expect a 10th byte at the end of the slice (the checksum). - Implement a simple checksum validation: sum all the first 9 bytes of the packet and check if the result modulo 256 equals the 10th byte.
- If the checksum is invalid, return a
Err("Checksum mismatch"). - Ensure that absolutely no
alloctypes (likeVecorString) are used.
Starter snippet:
fn validate_checksum(bytes: &[u8]) -> bool {
// Your logic here
}
There are no comments for now.