Item 16: Avoid writing unsafe code

The memory safety guarantees 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 re-organize your code to mollify the borrow checker (Item 15), and to precisely specify the pointer types that you use (Item 9).

Unsafe Rust weakens some of those guarantees, in particular by allowing 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 used.

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 1000 uses of unsafe in the alloc library, 1500 in core and a further 2000 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 9 use unsafe code (often raw pointers) internally in order 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.

The standard library1 also has other functionality covering more advanced features, implemented with unsafe internally:

  • std::pin::Pin forces an item to not move in memory (Item 14). This allows self-referential data structures, often a bête noire for new arrivals to Rust.
  • std::borrow::Cow provides a clone-on-write smart pointer: the same pointer can be used for both reading and writing, and a clone of the underlying data only happens if and when a write occurs.
  • 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 ecosystem also includes many crates that encapsulate unsafe code to provide a frequently-used feature. For example:

  • 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.

There are many other examples, but hopefully the general idea is clear. If you want to do something that doesn't obviously fit with the constraints of Rust (especially Item 15 and Item 14) hunt through the standard library to see if there's existing functionality that does what you need. If you don't find it, try also hunting through; after all, most of the time your problem will not be a unique one 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; see 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 of 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".

  • Write even more tests (Item 30) than usual.
  • Run additional diagnostic tools (Item 31) over the code. In particular, run Miri over your unsafe codeMiri interprets the intermediate level output from the compiler, which allows it to detect classes of errors that are invisible to the Rust compiler.
  • Think about multi-threaded use, particularly if there's shared state (Item 17).

1: In practice, most of thisstd functionality is actually provided by core, and so is available to no_std code as described in Item 33.