Rust’s safety guarantees and the ability to avoid undefined behavior are astonishing. Not everything in computing can be expressed in such an environment and for that reason Rust provides the ability to write a part of your code with safety requirements relaxed and the responsibility moved back to the developer.

Working around the borrow checker

Most of the time you use the unsafe code indirectly through the standard library or through additional libraries like Tokio that give you the necessary tools so that you don’t need to write your own unsafe code to handle common tasks.

For example, Rust’s data model doesn’t support shared ownership but Arc does exactly that for you. Similarly, Rust’s data model doesn’t support sharing of mutable references but Mutex, RefCell and Cell do exactly that for you. For common software development tasks it is usually preferable to use standard library or third-party library tools to avoid the need to reach for unsafe programming in Rust.

When you do need to use unsafe programming in Rust, the best practice is to approach it in the same way as the library writers and provide a good abstraction to shield the application code from the unsafe code as much as possible. Templates are a great tool to provide generic abstraction over the unsafe code.

Unsafe blocks and functions

You can use unsafe blocks to perform a whole lot of unsafe actions in the block. On the other hand, these are often used to just do a single unsafe thing and get the result.

let pointer: *mut Something = /* ... */;
let data = unsafe { ptr.as_mut().unwrap() };

Unsafe blocks inside ordinary (safe) functions are expected to be sound, meaning that the author of the function code is responsible to make the code safe from the perspective of the caller. His responsibility is to provide the same guarantees that would otherwise be provided by the compiler.

Unsafe functions are functions that opt out from the safe semantics and can only be called in unsafe blocks just like other unsafe operations. They are often used for highly optimized code or for unsafe conversions.

pub unsafe fn my_function() {
    /* ... */
}

You can also check the documentation for unsafe traits and unsafe trait implementations. These are out of scope of this document.

Raw pointers and phantom data

One of the use cases for unsafe code in Rust is memory access through raw pointers. These pointers are explicitly either *mut T or *const T and can be assigned a memory address including a null pointer value. This isn’t even restricted to unsafe code blocks. There’s also a special NonNull wrapper over a raw mutable pointer that isn’t null.

What is restricted to unsafe code is the dereferencing of such pointers, turning them into references, pointer arithmetics and more. When performing these restricted operations, you have to be careful to avoid undefined behavior.

Rust references are very strict since they do not allow mutable aliasing at all as you can never have two mutable references to the same location at once. In unsafe Rust, you are responsible to avoid that. You also need to avoid accessing the memory through the raw pointer while there exists a reference to it. These are the rules.

The easiest way to get a valid raw pointer is to use Box::new() for the dynamic allocation and then Box::into_raw() to turn the Box into a raw pointer. The easiest way to deallocate the memory is to turn the raw pointer back using Box::from_raw().

When you build your own data structures, you sometimes need type parameters or lifetime parameters but the compiler refuses to compile the template because you’re not actually using these in the code, you can add zero-size PhantomData marker that was created exactly for that purpose. When needed, by adding a PhantomData<&'a T> you can avoid the compilation error for both the T type and the 'a lifetime.

UnsafeCell

In some cases, like when introducing interior mutability like with a Cell a RefCell or a Mutex, it isn’t necessary to use a (separate) dynamic allocation. These are all immutable (or shareable) and in Rust it would be incorrect (and cause undefined behavior) to create a mutable reference to the contents of the cell or to modify it via a raw pointer.

One exception is the UnsafeCell that explicitly allows you to modify the contents of the cell or to have one mutable reference to it at a time. This is how Mutex is implemented. It doesn’t require a separate memory allocation for the data, instead the mutable data is stored directly in the memory of the immutable data structure. The Mutex unsafe code ensures the exclusive access.

You can create your own data structures with interior mutability using the UnsafeCell. This is especially useful when creating abstractions over operating system synchronization mechanisms.

Practical usage of unsafe Rust

You need unsafe programming to interface directly with other programming languages like C. But you can also use it to build data structures that do not fit in the ownership and borrowing model of safe Rust.

A doubly-linked list is a perfect example of what you can easily create in unsafe Rust but would be challenging in the safe language. You can easily build abstract data structures with templates and unsafe Rust. This is especially useful when you need to create your own optimized data structures for specific use cases.