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
198: Calling Unsafe Functions
By now, you've seen how Rust's borrow checker keeps you from shooting yourself in the foot. But here's the reality: sometimes you have to shoot yourself in the foot. Or, more accurately, you need to tell the compiler, "Trust me, I know what I'm doing," because you're doing something the compiler can't possibly verify—like talking to a C library or manipulating raw memory addresses.
Dealing with a raw pointer
Let's imagine we're writing a driver for a piece of hardware that gives us a raw pointer to a memory-mapped buffer. In a real scenario, this pointer would come from an external C library, but for this example, I'll just create a local array and grab a raw pointer to it. We want to convert this raw pointer into a Rust slice so we can actually use it with the tools we love, like iterators.
fn main() {
let data = [10, 20, 30, 40, 50];
let ptr = data.as_ptr(); // This is a *const i32
let len = data.len();
// I want to turn this pointer back into a slice...
let slice = std::slice::from_raw_parts(ptr, len);
println!("Buffer content: {:?}", slice);
}
If you try to compile this, Rust is going to stop you immediately. You'll see an error saying that std::slice::from_raw_parts is an unsafe function. Rust isn't saying the function is "broken"; it's saying that the compiler cannot possibly guarantee that ptr is valid or that len is correct. If I passed a random memory address here, the program would crash or, worse, leak sensitive data.
Entering the unsafe block
To call an unsafe function, we have to wrap it in an unsafe block. This is essentially a marker that says, "I've checked the invariants manually, and I'll take the blame if this segfaults."
fn main() {
let data = [10, 20, 30, 40, 50];
let ptr = data.as_ptr();
let len = data.len();
let slice = unsafe {
std::slice::from_raw_parts(ptr, len)
};
println!("Buffer content: {:?}", slice);
}
Now it compiles. But notice that only the function call is inside the block. I generally recommend keeping unsafe blocks as small as humanly possible. The smaller the block, the easier it is to audit when something goes wrong.
Where I messed up the bounds
Here is where things get dangerous. When I first wrote a similar utility for a project a few years ago, I made a classic mistake: I assumed the length of the buffer was a constant based on the hardware spec, rather than verifying the actual allocated size.
Let's simulate that mistake here. Watch what happens when I lie to the compiler about how much memory is actually there:
fn main() {
let data = [10, 20, 30, 40, 50];
let ptr = data.as_ptr();
// I'm claiming there are 100 elements here, but there are only 5.
let len = 100;
let slice = unsafe {
std::slice::from_raw_parts(ptr, len)
};
// This might print garbage, or it might crash.
// Either way, it's Undefined Behavior (UB).
println!("Buffer content: {:?}", slice);
}
This is why unsafe is scary. The compiler didn't warn me. It trusted me. I've now created a slice that reads past the end of my array and into memory I don't own. To fix this, I have to ensure that the len variable is strictly tied to the actual allocation size. In a real FFI (Foreign Function Interface) scenario, this means you must be absolutely certain that the C code providing the pointer also provided the correct length.
Wrapping unsafe in a safe abstraction
You should almost never ask your users to write unsafe blocks. Instead, the "Rust way" is to wrap the unsafety inside a safe function. You provide a safe interface, and you handle the dangerous parts internally, ensuring that no matter what the user passes in, the unsafe block remains sound.
fn get_buffer_slice(ptr: *const i32, len: usize) -> &'static [i32] {
// We assume the caller has provided a valid pointer and length.
// In a real app, we'd add as many checks as possible here.
unsafe {
std::slice::from_raw_parts(ptr, len)
}
}
fn main() {
let data = [10, 20, 30, 40, 50];
let ptr = data.as_ptr();
let len = data.len();
// The caller doesn't need to know about 'unsafe' anymore.
let slice = get_buffer_slice(ptr, len);
println!("Safe access: {:?}", slice);
}
By doing this, you've isolated the danger. If the program crashes during a memory read, you know exactly which function to look at.
📋 Practical Task
Exercise: Summing a Raw Memory Buffer
Your task is to create a safe wrapper function that interacts with a raw pointer to calculate the sum of an integer array.
- Create a function named
sum_raw_bufferthat takes two arguments: a raw pointerptr: *const i32and a lengthlen: usize. - Inside this function, use an
unsafeblock to convert the raw pointer and length into a Rust slice usingstd::slice::from_raw_parts. - Calculate the sum of the elements in that slice and return it as an
i32. - In your
mainfunction, create a standard array, get its raw pointer and length, and call yoursum_raw_bufferfunction to print the result.
There are no comments for now.