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
197: Raw Pointers in Depth
When do I actually need a raw pointer instead of just using a reference or a smart pointer?
In 99% of your Rust code, you won't. References are safer and smart pointers like Box or Arc handle the heavy lifting for you. But you'll hit a wall when you start doing FFI (Foreign Function Interface) to talk to C libraries, or when you're building the very data structures that those smart pointers are built upon.
Take a doubly linked list, for example. If you try to build one using standard references, you'll fight the borrow checker until you're blue in the face because you have multiple mutable paths to the same data. Raw pointers (*const T and *mut T) let you sidestep the borrow checker entirely. I usually tell people to view raw pointers as "trust me, I know what I'm doing" pointers. You're telling Rust, "Stop tracking this for a second; I'll handle the memory safety myself."
If I can't dereference them without unsafe, what's the point of creating them?
This is a common point of confusion. Here is the key: creating a raw pointer is perfectly safe. Dereferencing it is where the danger lies.
You can cast a reference to a raw pointer anywhere in your code without an unsafe block. This allows you to pass pointers around, store them in structs, or perform pointer arithmetic without needing to wrap your entire architecture in unsafe. You only enter the "danger zone" when you actually want to read or write the value at that address.
let mut value = 42;
let raw_ptr = &mut value as *mut i32; // This is safe!
unsafe {
// This is where the risk is. Is raw_ptr still valid?
// Does it point to null? Rust doesn't know, and neither does the compiler.
*raw_ptr = 100;
}
By separating creation from usage, Rust forces you to explicitly mark the exact line where memory safety is no longer guaranteed, making it much easier to audit your code when things inevitably crash.
How do I handle pointer arithmetic and nulls without crashing?
Unlike references, raw pointers can be null. If you're interfacing with a C API, you'll see std::ptr::null() and std::ptr::null_mut() everywhere. You should always check for null before dereferencing, or use the .as_ref() method which converts a raw pointer into an Option<T>.
As for arithmetic, you can use the .offset() or .add() methods. But be careful—stepping outside the bounds of your allocated memory is an immediate trip to Undefined Behavior (UB) town. I prefer .offset() when I'm dealing with buffers where I know the exact stride of the data.
let array = [10, 20, 30, 40];
let ptr = array.as_ptr();
unsafe {
// Move the pointer forward by 2 elements
let third_element = ptr.add(2);
println!("The third element is: {}", *third_element); // 30
}
Just remember: add() and offset() don't check bounds. If you add 10 to a pointer pointing to a 4-element array, Rust won't stop you, but your OS probably will with a segmentation fault.
📋 Practical Task
Build a Manual Memory Buffer Offset Reader
Your task is to create a small utility that simulates reading a structured binary packet from a raw memory buffer. This will force you to practice pointer creation, arithmetic, and unsafe dereferencing.
- Create a byte array
[u8; 12]containing some dummy data. - Create a raw pointer to the start of this array.
- Using
unsafe, implement a functionread_u32_at(ptr: *const u8, offset: usize) -> u32. - Inside that function, use
.add(offset)to move the pointer and then cast the resulting*const u8to a*const u32. - Dereference the pointer to return the
u32value. - Call this function twice to read two different 4-byte integers from your 12-byte buffer.
Hint: You'll need to use as *const u32 to cast the pointer before dereferencing it to read 4 bytes at once.
There are no comments for now.