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 use unsafe 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 use unsafe 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) in std::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 the unsafe_op_in_unsafe_fn lint so that explicit unsafe blocks are required when performing unsafe operations, even when those operations are performed in a function that is unsafe 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 codeMiri 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.


1

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.