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.