Item 16: Avoid writing
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 14), 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 38).
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 –
Arcand friends – described in Item 9 use
unsafecode internally in order to be able to present their particular semantics to their users.
- The synchronization primitives –
RwLockand associated guards – from Item 17 use
unsafe, OS-specific code internally.
The standard library1 also has other functionality covering more advanced features,
std::pin::Pinforces 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.
std::borrow::Cowprovides 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 (
std::memallow 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
unsafe code to provide a frequently-used feature. For example:
once_cellprovides a way to have something like global variables, initialized exactly once.
randprovides random number generation, making use of the lower-level underlying features provided by the operating system and CPU.
byteorderallows raw bytes of data to be converted to and from numbers.
cxxallows 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 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 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 38). 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
Also, if you're forced to write
unsafe code, pay attention to the warning implied by the keyword itself: "Hic sunt
- Write even more tests (Item 30) than usual.
- Run additional diagnostic tools (Item 31) over the code. In particular, run Miri over your
unsafecode – 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)