Skip to content
DocsRust LearningexpertUnsafe Rust
Chapter 19 of 19·expert·10 min read

Unsafe Rust

Rust Không An Toàn

Raw pointers, unsafe functions, and FFI

Hover or tap any paragraph to see Vietnamese translation

What is Unsafe Rust?

Rust is famous for its memory safety guarantees enforced at compile time. However, hidden inside safe Rust is a second language: Unsafe Rust. Unsafe Rust exists because the compiler's static analysis is inherently conservative — when the compiler cannot be sure code is correct, it will reject it, even if the code is actually safe.

The unsafe keyword does not turn off the borrow checker or disable Rust's other safety checks. It only gives you access to five features that the compiler does not check for memory safety. You still get safety inside an unsafe block.

Warning
Using unsafe is a contract between you and the compiler: you are saying "I know what I am doing and I guarantee the safety invariants are upheld." If you violate this contract, you get undefined behavior.

The 5 Unsafe Superpowers

In unsafe Rust, you can perform five actions that you cannot do in safe Rust. These are called the unsafe superpowers.

  • Dereference a raw pointer.
  • Call an unsafe function or method.
  • Access or modify a mutable static variable.
  • Implement an unsafe trait.
  • Access fields of a union.

It is important to understand that unsafe does not mean the code is necessarily dangerous. It means the responsibility for maintaining memory safety guarantees falls on the programmer instead of the compiler.

Raw Pointers

Unsafe Rust has two raw pointer types: *const T (immutable pointer) and *mut T (mutable pointer). The asterisk is part of the type name, not the dereference operator. Raw pointers differ from references and smart pointers in several ways.

  • Raw pointers are allowed to ignore the borrowing rules: you can have both immutable and mutable pointers or multiple mutable pointers to the same location.
  • Raw pointers are not guaranteed to point to valid memory.
  • Raw pointers are allowed to be null.
  • Raw pointers do not implement any automatic cleanup.

You can create raw pointers in safe code, but you cannot dereference them outside an unsafe block.

src/main.rs
1fn main() {2    let mut num = 5;34    // Creating raw pointers in safe code is fine5    let r1 = &num as *const i32;6    let r2 = &mut num as *mut i32;78    // Dereferencing requires an unsafe block9    unsafe {10        println!("r1 is: {}", *r1);11        println!("r2 is: {}", *r2);12    }1314    // Creating a pointer to an arbitrary memory address15    let address = 0x012345usize;16    let _r = address as *const i32;17    // Dereferencing this would likely cause a segfault!18}

When Raw Pointers Are Useful

Raw pointers are useful when interfacing with C code (FFI), when building safe abstractions the borrow checker cannot understand, or when you need maximum performance and can manually guarantee correctness.

Unsafe Functions and Blocks

Unsafe functions are functions that have preconditions the compiler cannot verify. You declare an unsafe function by adding the unsafe keyword before fn. The entire body of an unsafe function is treated as an unsafe block.

src/main.rs
1// An unsafe function — caller must ensure the pointer is valid2unsafe fn dangerous(ptr: *const i32) -> i32 {3    *ptr4}56fn main() {7    let value = 42;8    let ptr = &value as *const i32;910    // Must call unsafe functions inside an unsafe block11    let result = unsafe { dangerous(ptr) };12    println!("Result: {}", result);13}

Minimizing Unsafe Scope

A best practice is to keep unsafe blocks as small as possible. This makes it easier to audit and narrow down the source of any memory bugs.

src/main.rs
1fn main() {2    let mut data = vec![1, 2, 3, 4, 5];3    let ptr = data.as_mut_ptr();45    // Bad: large unsafe block6    // unsafe {7    //     let val = *ptr;8    //     let processed = val + 10;9    //     println!("{}", processed);10    //     // ... many more lines ...11    // }1213    // Good: minimal unsafe block14    let val = unsafe { *ptr };15    let processed = val + 10;16    println!("{}", processed);17}
Tip
When you encounter memory bugs, you only need to audit the unsafe blocks. Keeping them small significantly reduces debugging time.

Foreign Function Interface (FFI)

Rust can call functions written in other languages through FFI. This always requires unsafe because Rust cannot guarantee the safety of foreign code. You use extern blocks to declare foreign functions.

Calling C from Rust

src/main.rs
1// Declare external functions from the C standard library2extern "C" {3    fn abs(input: i32) -> i32;4    fn strlen(s: *const u8) -> usize;5}67fn main() {8    let x = -5;910    // Calling foreign functions is always unsafe11    let result = unsafe { abs(x) };12    println!("Absolute value of {} is {}", x, result);1314    let s = b"hello\0";  // null-terminated byte string15    let len = unsafe { strlen(s.as_ptr()) };16    println!("Length: {}", len);17}

The "C" string after extern specifies the ABI (Application Binary Interface) the foreign function uses. The "C" ABI is the most common and follows the C language calling convention.

Calling Rust from Other Languages

You can also allow other languages to call Rust functions. Use extern "C" on the function definition and the #[no_mangle] attribute to prevent the Rust compiler from mangling the function name.

src/lib.rs
1// This function can be called from C or other languages2#[no_mangle]3pub extern "C" fn call_from_c(x: i32) -> i32 {4    x * 25}67// In C, you would declare it as:8// int32_t call_from_c(int32_t x);

Safe Abstractions over Unsafe Code

The most common pattern in Rust is wrapping unsafe code in a safe API. The standard library does this extensively. The split_at_mut function is a classic example.

Let us try to implement split_at_mut ourselves. This function takes a mutable slice and splits it into two at a given index. The borrow checker will not let us borrow two different parts of the same slice because it only sees that we are borrowing from the same slice twice.

src/main.rs
1use std::slice;23fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {4    let len = values.len();5    let ptr = values.as_mut_ptr();67    assert!(mid <= len);89    // This would NOT compile with safe Rust:10    // (&mut values[..mid], &mut values[mid..])1112    // Safe abstraction using unsafe

Notice that split_at_mut has a safe signature — it is not marked unsafe. The unsafe code inside is protected by the assert! check, ensuring the index is within bounds. This is what a safe abstraction means: users do not need to know that unsafe is used internally.

Info
When creating safe abstractions, you must ensure that all valid inputs a user can provide will not cause undefined behavior. Use assert!, bounds checks, and type invariants to protect the unsafe code inside.

Mutable Static Variables

Rust supports global variables with the static keyword. Accessing and modifying mutable static variables is unsafe because multiple threads could access them simultaneously, leading to data races.

src/main.rs
1static mut COUNTER: u32 = 0;23fn add_to_count(inc: u32) {4    // Modifying a mutable static requires unsafe5    unsafe {6        COUNTER += inc;7    }8}910fn main() {11    add_to_count(3);12    add_to_count(7);1314    // Reading a mutable static also requires unsafe15    unsafe {16        println!("COUNTER: {}", COUNTER);17    }18}
Warning
In most cases, you should prefer concurrency-safe types like Mutex or AtomicU32 instead of mutable statics. Mutable statics are very prone to data races.

Unsafe Traits and Unions

Unsafe Traits

A trait is unsafe when at least one of its methods has an invariant the compiler cannot verify. You declare an unsafe trait and use unsafe impl to implement it.

src/main.rs
1// A trait with safety invariants the compiler cannot check2unsafe trait Zeroable {3    // Types implementing this must be valid when all bits are zero4}56// We promise that i32 is valid when zero-initialized7unsafe impl Zeroable for i32 {}8unsafe impl Zeroable for u64 {}910fn zero_init<T: Zeroable>() -> T {11    unsafe { std::mem::zeroed() }12}1314fn main() {15    let x: i32 = zero_init();16    let y: u64 = zero_init();17    println!("x = {}, y = {}", x, y);18}

Unions

A union is similar to a struct but all fields share the same memory. Accessing union fields is unsafe because Rust cannot guarantee the type currently stored is the correct one.

src/main.rs
1union IntOrFloat {2    i: i32,3    f: f32,4}56fn main() {7    let u = IntOrFloat { i: 42 };89    // Accessing a union field requires unsafe10    let value = unsafe { u.i };11    println!("As integer: {}", value);1213    // Interpreting the same bits as a float14    let u2 = IntOrFloat { f: 3.14 };15    let bits = unsafe { u2.i };16    println!("Float bits as integer: {}", bits);17}

When to Use Unsafe

Unsafe is a powerful tool but should be used carefully. Here are guidelines for deciding when to use unsafe.

  • Use unsafe when you need to interface with C code or the operating system (FFI). This is the most legitimate reason.
  • Use unsafe when the borrow checker is too conservative and you can prove correctness. The split_at_mut example above illustrates this.
  • Use unsafe for performance optimizations when truly needed and measured. For example: skipping bounds checks with get_unchecked.
  • Avoid unsafe when a reasonable safe solution exists. Safe code is easier to maintain and less buggy.
  • Always wrap unsafe in safe abstractions. Do not let unsafe code spread across the codebase.
Warning
Undefined behavior (UB) in Rust includes: dereferencing null or dangling pointers, reading uninitialized memory, violating pointer aliasing rules, creating null references, and data races. If your unsafe code causes any of these, your program has undefined behavior.

Key Takeaways

Điểm Chính

  • unsafe blocks unlock 5 superpowers the borrow checker cannot verifyKhối unsafe mở khóa 5 siêu năng lực mà borrow checker không thể xác minh
  • Raw pointers (*const T, *mut T) can be null or danglingCon trỏ thô (*const T, *mut T) có thể null hoặc treo
  • FFI uses extern "C" to call functions across language boundariesFFI dùng extern "C" để gọi hàm qua ranh giới ngôn ngữ
  • Minimize unsafe scope and wrap it in safe public APIsGiảm thiểu phạm vi unsafe và bọc nó trong API công khai an toàn

Practice

Test your understanding of this chapter

Quiz

How many unsafe superpowers does Rust provide?

Rust cung cấp bao nhiêu siêu năng lực unsafe?

True or False

Creating a raw pointer requires an unsafe block.

Tạo con trỏ thô yêu cầu khối unsafe.

Quiz

What does the #[no_mangle] attribute do when used with extern functions?

Thuộc tính #[no_mangle] làm gì khi sử dụng với hàm extern?

True or False

The unsafe keyword disables all of Rust's safety checks including the borrow checker.

Từ khóa unsafe vô hiệu hóa tất cả kiểm tra an toàn của Rust bao gồm cả bộ kiểm tra mượn.

Code Challenge

Complete the code to dereference a raw pointer

Hoàn thành mã để giải tham chiếu con trỏ thô

let val =  { *ptr };

Course Complete!

You've completed the entire Rust Learning course. Excellent work!

Your progress0 of 19 chapters read
← → to navigate chapters
Built: 4/8/2026, 12:01:11 PM