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
200: Creating Safe Abstractions Over Unsafe Code
You've already learned how to use the unsafe keyword to tell the compiler, "Trust me, I know what I'm doing." But in a real production codebase, you almost never want your teammates (or your future self) to have to write unsafe blocks every time they want to use a specific feature. That's where safe abstractions come in. The goal is to build a "safe wall" around a dangerous core.
Why can't I just use safe Rust for everything?
In a perfect world, you could. But you'll eventually hit a wall where you need to interface with a C library via FFI, perform extremely high-performance pointer arithmetic, or implement a low-level data structure like a circular buffer or a doubly-linked list. Rust's borrow checker is amazing, but it's conservative; it will reject some programs that are actually correct because it can't prove they are safe.
I've found that the best approach isn't to avoid unsafe entirely, but to isolate it. If you encapsulate the danger inside a small, heavily audited module, the rest of your application can remain 100% safe. You're essentially trading a global risk for a local one.
How do I actually "guarantee" safety if the internals are unsafe?
The secret is invariants. An invariant is a condition that your code must always maintain to remain sound. If you're wrapping a raw pointer, your invariant might be: "This pointer always points to valid memory, and it always points to exactly len bytes."
Look at this example. I'm building a very simple SafeBuffer that manages a raw heap allocation. Notice how the raw pointer is private, but the methods are public and safe.
use std::alloc::{alloc, dealloc, Layout};
use std::ptr;
pub struct SafeBuffer {
ptr: *mut u8,
len: usize,
}
impl SafeBuffer {
pub fn new(size: usize) -> Self {
let layout = Layout::array::(size).unwrap();
unsafe {
// The danger is here, but the user of SafeBuffer never sees this.
let ptr = alloc(layout);
if ptr.is_null() {
panic!("Allocation failed");
}
SafeBuffer { ptr, len: size }
}
}
pub fn get(&self, index: usize) -> Option<&u8> {
if index >= self.len {
return None; // We maintain safety by bounds-checking here.
}
unsafe {
// We can safely dereference because we've checked the bounds
// and we know the pointer is valid for the lifetime of 'self'.
Some(&*self.ptr.add(index))
}
}
}
By making the ptr field private, I've ensured that no one outside this struct can accidentally increment the pointer or free the memory prematurely. The get method is the "gatekeeper"βit enforces the bounds check so the unsafe block inside is actually safe.
What happens if I forget to clean up the raw memory?
If you're using unsafe to allocate memory, Rust's automatic memory management doesn't know how to handle it. You'll end up with a memory leak. To fix this, you need to implement the Drop trait. This is where you define the "cleanup" logic that runs when the abstraction goes out of scope.
impl Drop for SafeBuffer {
fn drop(&mut self) {
let layout = Layout::array::(self.len).unwrap();
unsafe {
// We must use the same layout we used for allocation.
dealloc(self.ptr, layout);
println!("Memory safely cleaned up!");
}
}
}
I'll be honest: this is where most bugs in safe abstractions hide. If you accidentally implement Clone for a struct like this without doing a "deep copy" of the memory, you'll end up with a double-free error when both clones try to dealloc the same pointer. If you're wrapping raw pointers, be extremely careful with Clone and Copy.
Do I need to document the unsafe parts differently?
Yes. In the Rust community, there's a convention called the # Safety section. When you write a function that requires the caller to uphold certain conditions to avoid Undefined Behavior, you explicitly document it. However, for the SafeBuffer above, the goal was to make the public API safe. The internal unsafe blocks should still have comments explaining why they are sound.
- Wrong:
unsafe { // just do it } - Right:
unsafe { // Safe because we checked index < self.len above }
π Practical Task
Building a SafeRawIntegerArray
Your task is to create a safe abstraction called SafeRawIntegerArray that manages a raw pointer to a block of i32 integers. You need to ensure that the user can never access memory outside the allocated range and that memory is properly freed when the array is dropped.
Requirements:
- Implement a
new(size: usize)method that allocates memory forsizenumber ofi32elements usingstd::alloc::alloc. - Implement a
set(&mut self, index: usize, value: i32)method. This must be a safe function that performs bounds checking before usingunsafeto write the value to the pointer. - Implement a
get(&self, index: usize) -> Option<i32>method. This must be a safe function that performs bounds checking. - Implement the
Droptrait to ensurestd::alloc::deallocis called with the correctLayout.
Testing your implementation:
Try creating an array of size 5, setting the value at index 4, and then attempting to set a value at index 10. Your code should handle the out-of-bounds access gracefully (e.g., by panicking or returning a Result/Option) without triggering a segmentation fault.
There are no comments for now.