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 useunsafe
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 useunsafe
, 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
) 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. 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 crates.io
;
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
code – Miri 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.