Item 16: Avoid writing unsafe
code
The memory safety guarantees—without runtime overhead—of Rust are its unique selling point; it is the Rust language feature that is not found in any other mainstream language. These guarantees come at a cost: writing Rust requires you to reorganize your code to mollify the borrow checker (Item 15) and to precisely specify the reference types that you use (Item 8).
Unsafe Rust is a superset of the Rust language that weakens some of these restrictions—and the corresponding
guarantees. Prefixing a block with the unsafe
keyword switches that block into unsafe mode, which allows things
that are not supported in normal Rust. In particular, it allows the use of raw pointers that work
more like old-style C pointers. These pointers are not subject to the borrowing rules, and the programmer is responsible
for ensuring that they still point to valid memory whenever they're dereferenced.
So at a superficial level, the advice of this Item is trivial: why move to Rust if you're just going to write C code in
Rust? However, there are occasions where unsafe
code is absolutely required: for low-level library code or for when
your Rust code has to interface with code in other languages (Item 34).
The wording of this Item is quite precise, though: avoid writing unsafe
code. The emphasis is on the
"writing", because much of the time, the unsafe
code you're likely to need has already been written for you.
The Rust standard libraries contain a lot of unsafe
code; a quick search finds around 1,000 uses of unsafe
in the
alloc
library, 1,500 in core
, and a further 2,000 in std
. This code has been written by experts and is
battle-hardened by use in many thousands of Rust codebases.
Some of this unsafe
code happens under the covers in standard library features that we've already covered:
- The smart pointer types—
Rc
,RefCell
,Arc
, and friends—described in Item 8 useunsafe
code (often raw pointers) internally to be able to present their particular semantics to their users. - The synchronization primitives—
Mutex
,RwLock
, and associated guards—from Item 17 useunsafe
OS-specific code internally. Rust Atomics and Locks by Mara Bos (O'Reilly) is recommended if you want to understand the subtle details involved in these primitives.
The standard library also has other functionality covering more advanced
features, implemented with unsafe
internally:1
std::pin::Pin
forces an item to not move in memory (Item 15). This allows self-referential data structures, often a bête noire for new arrivals to Rust.- The
std::sync::atomic
module provides atomic versions of primitive types. - Various functions (
take
,swap
,replace
) instd::mem
allow items in memory to be manipulated without falling foul of the borrow checker.
These features may still need a little caution to be used correctly, but the unsafe
code has been encapsulated in a
way that removes whole classes of problems.
Moving beyond the standard library, the crates.io
ecosystem also includes many crates that
encapsulate unsafe
code to provide a frequently used feature:
once_cell
: Provides a way to have something like global variables, initialized exactly once.rand
: Provides random number generation, making use of the lower-level underlying features provided by the operating system and CPU.byteorder
: Allows raw bytes of data to be converted to and from numbers.cxx
: Allows C++ code and Rust code to interoperate (also mentioned in Item 35).
There are many other examples, but hopefully the general idea is clear. If you want to do something that doesn't
obviously fit within the constraints of Rust (especially Item 14 and Item 15), hunt through the standard library to see
if there's existing functionality that does what you need. If you don't find what you need, try also hunting through
crates.io
. After all, it's unusual to encounter a unique problem that no one else has ever faced before.
Of course, there will always be places where unsafe
is forced, for example, when you need to interact with code written
in other languages via a foreign function interface (FFI), as discussed in Item 34. But when it's necessary,
consider writing a wrapper layer that holds all the unsafe
code that's required so that other programmers
can then follow the advice given in this Item. This also helps to localize problems: when something goes wrong, the unsafe
wrapper can be the first suspect.
Also, if you're forced to write unsafe
code, pay attention to the warning implied by the keyword itself: Hic sunt
dracones.
- Add safety comments that document the
preconditions and invariants that the
unsafe
code relies on. Clippy (Item 29) has a warning to remind you about this. - Minimize the amount of code contained in an
unsafe
block, to limit the potential blast radius of a mistake. Consider enabling theunsafe_op_in_unsafe_fn
lint so that explicitunsafe
blocks are required when performingunsafe
operations, even when those operations are performed in a function that isunsafe
itself. - Write even more tests (Item 30) than usual.
- Run additional diagnostic tools (Item 31) over the code. In particular, consider running Miri over your
unsafe
code—Miri interprets the intermediate level output from the compiler, that allows it to detect classes of errors that are invisible to the Rust compiler. - Think carefully about multithreaded use, particularly if there's shared state (Item 17).
Adding the unsafe
marker doesn't mean that no rules apply—it means that you (the programmer) are now
responsible for maintaining Rust's safety guarantees, rather than the compiler.
In practice, most of this std
functionality is actually provided by core
and so is
available to no_std
code as described in Item 33.