Rust is famous for its safe programming languages, and it automatically checks for memory safety, prevents data races, and protects your programs from many common bugs at compile time.
Rust allows you to write unsafe code for your program to better interact with memory, hardware, or external systems.
Unsafe code is a means to understand the risks associated with programs and then mitigate them. It allows you to bypass Rust’s strict safety checks and perform low-level operations that would normally be considered unsafe.
Let’s understand the real reasons why unsafe code exists:
1) Working with Raw Pointers
Rust normally prevents you from directly handling memory addresses, and this is one of the best features that keeps it memory-safe.
But in some advanced use cases, you need to manage memory manually, interact with memory-mapped hardware, or implement your own memory allocator. In these cases, raw pointers const T and mut T are required, and these are only used inside an unsafe block.
fn main() {
let num = 42;
let ptr = &num as *const i32;
unsafe {
println!("Value: {}", *ptr); // Dereferencing a raw pointer
}
}
- In the above code, ptr is unsafe because Rust cannot guarantee that the pointer is valid, so we need to mark it as unsafe.
2) Calling External C Libraries (FFI)
Rust is used alongside C or C++ libraries, especially for system-level programming or reusing existing code.
These libraries are known as FFI (Foreign Function Interface). But Rust can’t verify the safety of external code, so it requires an unsafe block.
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value: {}", abs(-42));
}
}
3) Accessing Hardware or System Resources
Developers need direct access to hardware registers or memory locations. These operations are inherently unsafe because the compiler can’t protect you from hardware-specific issues, but this is necessary for low-level programming.
For example, writing a byte to a memory-mapped register might involve writing to a specific address. Rust can’t verify whether that address is valid, so you must do it in an unsafe block.
4) Low-Level Performance Optimizations
Sometimes, you may need to bypass certain safety checks to achieve maximum performance. These operations can be safe if done correctly, but Rust will require you to mark them as unsafe.
fn get_unchecked(slice: &[i32], index: usize) -> i32 {
unsafe { *slice.get_unchecked(index) }
}
fn main() {
let numbers = [1, 2, 3, 4];
println!("Value: {}", get_unchecked(&numbers, 2));
}
How to Write Unsafe Code in Rust
Unsafe code is written inside an unsafe block. This tells the compiler that the programmer takes responsibility for ensuring the safety inside that block.
Syntax for Unsafe Code
unsafe {
// Unsafe operations go here
}
Examples of Unsafe Code
Calling Unsafe Functions
Some functions are marked as unsafe because their safety cannot be guaranteed by the compiler.
unsafe fn dangerous_function() {
println!("This is an unsafe function");
}
fn main() {
unsafe {
dangerous_function(); // Calling an unsafe function
}
}
Modifying Static Variables
Static variables in Rust are shared across the entire program. Modifying mutable static variables requires unsafe code.
static mut COUNTER: i32 = 0;
fn increment_counter() {
unsafe {
COUNTER += 1;
println!("Counter: {}", COUNTER);
}
}
fn main() {
increment_counter();
increment_counter();
}
Explanation:
- static mut creates a mutable static variable.
- An unsafe block is required to modify it since it can lead to data races.
Implementing Unsafe Traits
Most traits are completely safe to implement, but sometimes, the trait’s correct behavior depends on certain safety guarantees that the compiler cannot check for you.
For example:
// Defining an unsafe trait
unsafe trait DangerousAction {
fn trigger();
}
// Implementing the unsafe trait
struct Machine;
unsafe impl DangerousAction for Machine {
fn trigger() {
println!("Machine triggered! Handle with care.");
}
}
fn main() {
let m = Machine;
// Calling the method from an unsafe trait
unsafe {
m.trigger();
}
}
Risks of Unsafe Code
Unsafe code can introduce the following risks:
Memory Leaks: The ownership system ensures memory is automatically cleaned up when it’s no longer needed. But in unsafe code, you allocate memory manually, and if you forget to free it, it stays allocated forever.
Null Pointer Dereferencing: A pointer is simply a memory address. In safe Rust, references are always valid; the compiler ensures you never read or write memory that doesn’t exist.
But with unsafe code, you can create raw pointers that can point to invalid or null memory.
Data Races: One of Rust’s greatest strengths is its fearless concurrency, which prevents data races at compile time. But unsafe code can bypass these checks, leading to multiple threads accessing the same data simultaneously without synchronization.
Undefined Behavior: Perhaps the most critical danger of unsafe code is undefined behavior when your program behaves in completely unpredictable ways because you broke Rust’s safety guarantees.
Learn Other Topics About Rust
- What are threads in Rust?
- What is concurrency in Rust?
- What is error handling in Rust?
- What is an HashMap in Rust?
- What is an Iterator in Rust?
- What is Mutex in Rust?
- What is Async in Rust?

M.Sc. (Information Technology). I explain AI, AGI, Programming and future technologies in simple language. Founder of BoxOfLearn.com.