Unlock Your Python Backend Career: Build 30 Projects in 30 Days. Join now for just $54

Rust Advanced

Unsafe in Rust

Introduction to unsafe Rust and its use cases

We have been writing memory-safe code all along that satisfies the Rust compiler. Unsafe Rust code does not bypass the borrow checker or disable the compiler. Instead, it gives access to several features:

  • Dereferencing a raw pointer

  • Calling a function or method unsafe

  • Accessing or modifying a mutable static variable

  • Implementing a trait unsafe

  • Accessing fields of unions

It doesn’t mean that the code in the unsafe block is neither safe nor dangerous. It simply means that the responsibility for ensuring safety shifts from the compiler to the programmer. Unsafe Rust allows you to perform operations that the compiler cannot verify for safety at compile-time, but it still enforces certain rules and checks to minimize potential issues.

Let's delve into some of the key use cases of unsafe Rust and understand when it might be necessary:

  1. Low-Level System Programming: When dealing with low-level operations such as interacting with hardware, memory-mapped registers, or C libraries, unsafe Rust comes into play. These tasks often require direct memory manipulation and interaction with external code, which cannot always be expressed within Rust's strict safety guarantees.

    // Example: Interfacing with a C library
    extern "C" {
        fn some_c_function(arg: u32) -> u32;
    }
    let result = unsafe {
        some_c_function(42)
    };
    
  2. Optimizations: There are situations where you, as the programmer, can reason about the code's safety better than the compiler. This could involve avoiding unnecessary bounds checks or using memory layout for performance gains.

    // Example: Using unsafe for performance optimization
    let slice = unsafe { std::slice::from_raw_parts_mut(ptr, len) };
    
  3. Building Safe Abstractions: Sometimes, writing safe, high-level abstractions requires dipping into unsafe code to provide the necessary guarantees. Libraries like std::slice::from_raw_parts_mut and std::mem::transmute use unsafe internally to provide safe interfaces for higher-level code.

    // Example: Building a safe abstraction using unsafe
    pub fn safe_function(data: &mut [u8]) {
        let slice = unsafe { std::slice::from_raw_parts_mut(data.as_mut_ptr(), data.len()) };
        // Now slice can be manipulated safely in safe_function
    }
    

Unsafe blocks and operations

Unsafe code is contained within unsafe blocks. This confines the potentially unsafe operations to a limited scope, making it easier to manage and review. Within an unsafe block, you can perform various operations that Rust's safety checks would normally prevent.

  • Dereferencing Raw Pointers: Raw pointers allow direct access to memory locations without ownership or borrowing semantics. This can lead to memory unsafety if not used carefully.

    let mut value = 42;
    let raw_ptr = &mut value as *mut i32;
    unsafe {
        *raw_ptr = 100;
    }
    
  • Calling Unsafe Functions or Methods: Some functions require unsafe blocks to be called because the compiler cannot verify their behavior.

    unsafe fn unsafe_function() {
        // Unsafe operations here
    }
    

Writing safe abstractions using unsafe code

Unsafe code is often used to build higher-level abstractions that are safe to use. While the unsafe code is contained, the public interface remains safe for users.

For example, consider implementing a linked list where pointers are manipulated within an unsafe block, but the public API enforces Rust's ownership rules.

pub struct LinkedList<T> {
    head: Option<Box<Node<T>>>,
}

struct Node<T> {
    data: T,
    next: *mut Node<T>,
}

impl<T> LinkedList<T> {
    pub fn push(&mut self, data: T) {
        let new_node = Box::new(Node {
            data,
            next: std::ptr::null_mut(),
        });
        let raw_node = Box::into_raw(new_node);
        unsafe {
            if let Some(ref mut head) = self.head {
                (*raw_node).next = head.as_mut();
            }
            self.head = Some(Box::from_raw(raw_node));
        }
    }
}

In this example, the LinkedList struct maintains Rust's ownership semantics while using unsafe code internally to manipulate pointers within a controlled context.

In conclusion, unsafe Rust is a powerful tool that allows developers to write code that goes beyond the usual safety guarantees while still adhering to Rust's rules as much as possible. It enables low-level programming, performance optimizations, and the construction of safe abstractions for complex tasks. However, its usage comes with a higher responsibility for ensuring the safety and correctness of the code.

Exercise

  1. Unsafe Abstractions: Design a data structure like a doubly linked list or binary tree using unsafe Rust. Implement insertion, deletion, and traversal methods while carefully handling memory safety.

  2. Unsafe Blocks: Create a program that interacts with a C library through FFI. Use unsafe blocks to call C functions and handle the integration, ensuring memory safety and proper resource management.

Whenever you're ready

There are 4 ways we can help you become a great backend engineer:

The MB Platform

Join 1000+ backend engineers learning backend engineering. Build real-world backend projects, learn from expert-vetted courses and roadmaps, track your learnings and set schedules, and solve backend engineering tasks, exercises, and challenges.

The MB Academy

The “MB Academy” is a 6-month intensive Advanced Backend Engineering BootCamp to produce great backend engineers.

Join Backend Weekly

If you like post like this, you will absolutely enjoy our exclusive weekly newsletter, Sharing exclusive backend engineering resources to help you become a great Backend Engineer.

Get Backend Jobs

Find over 2,000+ Tailored International Remote Backend Jobs or Reach 50,000+ backend engineers on the #1 Backend Engineering Job Board