Reasons abound for Rust’s growing popularity: it’s fast, memory-safe without needing garbage collection, and outfitted with world-class tooling. Rust also allows experienced programmers to selectively toggle off some—although not all—of its safeties for the sake of speed or direct low-level memory manipulation.
“Unsafe” Rust is the general term for any Rust code that is contained in a block delineated by the unsafe
keyword. Inside an unsafe
block, you can bend (but not break) some (but not all) of Rust’s safety rules.
If you’ve come from C or another low-level systems language, you may be tempted to reach for unsafe
whenever you want to use some familiar pattern for manipulating things in a low-level way. In some cases, you may be right: there are a few things you can’t do in Rust except through unsafe
code. But in many cases you don’t actually need unsafe
. Rust already has you covered if you know where to look.
In this article, we’ll explore what unsafe
Rust is actually for, what it can and can’t do, and how to use it sensibly.
What you can do with ‘unsafe’ Rust
The unsafe
keyword in Rust lets you delineate a block of code, or function, to enable a specific subset of features in the language. Let’s look at the features you can access inside Rust’s unsafe
blocks.
Raw pointers
Rust’s raw pointers can refer to mutable or immutable values and are closer to C’s idea of a pointer than Rust’s references. With them, you can ignore some of the rules for how borrowing works:
- Raw pointers can be null values.
- Multiple raw mutable pointers can point to the same space in memory.
- You can also use both immutable and mutable pointers to refer to the same memory.
- You don’t need to guarantee that a raw pointer points to a valid region of memory.
Raw pointers are useful if you need to do things like access hardware directly (e.g., for a device driver), or talk to an application written in another language by way of a raw region of memory.
External function calls
Another common use of unsafe
is to make calls through a foreign function interface, or FFI. There’s no guarantee that what we get back from such a call will follow Rust’s rules, and there’s also a chance we will need to supply things that don’t conform to those rules (such as a raw pointer).
Consider this example (from Rust’s documentation):
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
Any calls made to the functions exposed via the extern "C"
block must be wrapped in unsafe
, the better to ensure you take proper responsibility for what you send to it and get back from it.
Altering mutable static variables
Global or static variables in Rust can be set to mutable
, since they occupy a fixed memory address. However, it’s only possible to modify a mutable static variable inside an unsafe
block.
Data races are the biggest reason you need unsafe
to alter mutable static variables. You’d get unpredictable results if you allowed the same mutable static variable to be modified from different threads. So, while you can use unsafe
to make such changes, any data race issues would be your responsibility, not Rust’s. In general, Rust cannot entirely prevent data races, but you need to be doubly cautious about that in unsafe
blocks.
Creating unsafe methods and traits
Methods (functions) can be made unsafe with the declaration unsafe fn
. You’d use this to ensure that any call to such a method must also be performed inside an unsafe
block.
For instance, if you had a function that required a raw pointer as an argument, you’d want to ensure the caller did their due diligence for how the call was performed in the first place. Any safeties don’t begin and end at the function-call boundary.
You can also declare traits unsafe, along with their implementations, using a similar syntax: unsafe trait
for the trait, and unsafe impl
for the implementation.
Unlike an unsafe method, though, an unsafe trait implementation does not have to be called inside an unsafe block. The burden of safety is on the one writing the implementation, not the one calling it.
Unions
Unions in Rust are essentially the same as unions in C: a struct that has multiple possible type definitions for its contents. This kind of loosey-goosey behavior is acceptable in C, but Rust’s sterner promises of correctness and safety don’t allow it by default.
However, sometimes you need to create a Rust structure that maps to a C union, such as when you call into a C library to work with a union. To do this, you need to use unsafe
to access one particular field definition at a time.
Here’s an example from the Comprehensive Rust guide:
#[repr(C)]
union MyUnion {
i: u8,
b: bool,
}
fn main() {
let u = MyUnion { i: 42 };
println!("int: {}", unsafe { u.i });
println!("bool: {}", unsafe { u.b }); // Undefined behavior!
}
For each access to the union, you have to use unsafe
. The borrow checker also requires borrowing all the fields of a union even if you just want to access one of them at a time.
Note that writing to a union does not have the same restrictions: in Rust’s eyes you’re not writing to anything that needs tracking. That’s why you don’t need unsafe
when defining the contents of the union with the let
statement.
What you cannot do with Rust ‘unsafe’
Outside of the four big points listed above, unsafe
doesn’t give you any other special powers.
The single biggest thing you can’t do is use unsafe
to circumvent the borrow checker. Borrows are still enforced on values in unsafe
, same as anyplace else. One of Rust’s truly immutable principles is how borrowing and references work, and unsafe
does not alter those rules.
For a good example of this, look at the way borrowing is still enforced in unions, as described in the previous section. See Steve Klabnik’s blog post for a more detailed discussion on this topic. It’s better to think of unsafe
as a superset of Rust—something that adds a few new features without taking away existing ones.
Best practices with unsafe code
unsafe
is like any other language feature: it has its uses and limitations, so use it judiciously and with care. Here are some pointers.
Wrap as little code as possible in ‘unsafe’
The smaller an unsafe
block is, the better. In many cases, you don’t need to make unsafe
blocks more than a couple of lines. It’s worth thinking about how much of the code actually needs to be unsafe, and how to enforce boundaries and interfaces around that code. It’s best to have safe interfaces to code that’s not safe.
Sometimes the interfaces themselves have to be unsafe. I mentioned earlier that you can declare an entire function as unsafe—for example, unsafe fn
. If an argument to a function requires unsafe
, any calls to such a function also must be unsafe
.
On the whole, though, it’s best to start with individual unsafe
blocks, and promote them to functions only if needed.
Be mindful of undefined behaviors
Undefined behaviors exist in Rust, and so they also exist in unsafe
Rust. For instance, Rust’s basic safeties don’t guard against data races, or reading from uninitialized memory. Be extra careful of how you might be introducing undefined behavior in your unsafe
blocks.
For examples of what to avoid, the Rust Reference has a long but not exhaustive list of undefined behavior.
Document why ‘unsafe’ is needed
It’s been said that we comment code not to say what is being done, but why. That idea absolutely applies to unsafe
blocks.
Whenever possible, document exactly why unsafe
is needed at any given point. This gives others some idea of the rationale behind the use of unsafe
, and—potentially—future insight into how the code could be rewritten to not need unsafe
.
Read the Rustonomicon
Rust’s documentation is second to none. That includes the Rustonomicon, an entire book-length document created to thoroughly document “all the awful details that you need to understand when writing Unsafe Rust programs.” Don’t open it until you are comfortable with Rust generally, as it delves into great detail about how “unsafe” and “regular” Rust interoperate.
If you’re coming in from the C side of things, another useful document is Learn Rust The Dangerous Way. This collection of articles talks about Rust in a way that people used to low-level C programming can wrap their heads around, including best practices for using Rust’s unsafe
feature.