Unsafe

From Rust Community Wiki
Jump to navigation Jump to search

Rust's safety guarantees can be limiting when writing low-level code, because they prevent you from doing potentially dangerous things. Unsafe Rust allows you to sidestep some of these restrictions.

Unsafe Rust is very dangerous and can cause undefined behavior (UB). Because of this, it is always marked with the unsafeThis links to official Rust documentation keyword. This makes it clear which parts of the code must be carefully vetted for safety. Furthermore, many Rust programs don't use unsafe at all. It is good practice to always add a comment to unsafe code, which explains why the code is safe, or what is required to use it safely.

Note that unsafe code is not allowed to violate Rust's safety guarantees. If unsafe is used, it's the programmer's responsibility to prevent undefined behavior, although the compiler can't verify this.

Unsafe operations[edit | edit source]

Using unsafe, it is possible to perform the following operations, which are not allowed elsewhere: [1][2]

Unsafe contexts[edit | edit source]

An unsafe context is a context where the unsafe operations are allowed:

  • unsafe { } blocks within a function introduce a scope with an unsafe context.
  • unsafe functions currently introduce an unsafe context as well, but this will be changed in the future.[3]
  • unsafe traits are implemented by writing unsafe impl.

Implications for safe code[edit | edit source]

When unsafe code is used incorrectly, it can cause UB; we call such code unsound. Unfortunately, even safe code can be unsound, if it somehow relies on an unsound API. This means that incorrect usage of unsafe can cause problems in different parts of the code and even different crates.

Examples[edit | edit source]

This example shows how to document unsafe functions following best practices:

/// Converts a `u8` to a `bool`
///
/// # Safety
///
/// Callers of this function are responsible that
/// these preconditions are satisfied:
///
///  * The `u8` must be either 0 or 1
///
unsafe fn num_to_bool(num: u8) -> bool {
    std::mem::transmute(num)
}

This example uses the above function:

fn get_bit_of(num: u8, bit_index: u8) -> bool {
    assert!(bit_index < 8);
    let num = (num >> bit_index) & 1;

    // Safety:
    // Because of the bitwise AND operation above, num is
    // guaranteed to be 0 or 1, so the following is safe
    unsafe { num_to_bool(num) }
}

Writing unsafe code[edit | edit source]

If you think that you need to resort to unsafe, there are some things you should consider:

  • Read the article about Undefined Behavior and the relevant parts of the Rustonomicon and the Unsafe Code Guidelines.
  • When writing unsafe code, document it carefully.
  • Benchmark your code or look at the generated assembly, to verify that the unsafe code actually performs better than the equivalent safe code.
  • Ask other programmers for a code review.
  • Run your code in Miri.
  • Add unit tests to your code.
  • Fuzz your code.

Building safe abstractions[edit | edit source]

One of Rust's key patterns is isolating unsafe code into small, manageable sections, each performing a specific distinct task. This enables higher-level logic to only use idiomatic safe Rust code, without worrying about unsafe details — and that property of safe code is one of Rust's selling points as a whole. Ideally, all unsafe code should be designed with the intent to wrap potentially unsafe operations in a safe interface, "converting" undefined behavior conditions to panics and errors.

The term "safe abstraction" refers to one specific way of wrapping a certain set of unsafe operations into a safe public interface, and one single set of unsafe operations does not necessarily have one single way of being wrapped in a safe abstraction.

RefCellThis links to official Rust documentation as an example of a safe abstraction[edit | edit source]

This shared mutability container is a great example of how a safe abstraction can wrap an unsafe interace in its own way with its own strong points and drawbacks. The implementation backbone is UnsafeCellThis links to official Rust documentation — on top of that unsafe primitive, RefCell creates a safe interface, using runtime checks to ensure the validity of operations. Here's a typical formulation of this relationship: "RefCell is a safe abstraction over UnsafeCell which uses borrow checker styled checks at runtime to ensure safety".

RefCell, has its drawbacks, however, which may or may not matter in a specific use case. The primary trade-off is that because borrowing rules are enforced at runtime, the borrow checker cannot help with checking accesses to the contents of the RefCell — the only way to know whether the code is correct is to actually see how it runs. The whole point of RefCell, however, is to provide more complex borrowing patterns which cannot be accepted by the borrow checker, which means that those complex borrowing patterns can be really hard to test for correctness. Another trade-off is that RefCell cannot be shared between threads.

Most unsafe operations cannot be exhaustively covered by just one implementation of a safe abstraction. Considering again the RefCell/UnsafeCell example, there are alternatives to RefCell which wrap the same unsafe internals — UnsafeCell — using different semantics, different interface and a different set of trade-offs. Those include, but are not limited to:

  • CellThis links to official Rust documentation, which does not perform checks of any kind — it has better performance, at the cost of not being able to return references to the contents (operating only with setThis links to official Rust documentation/getThis links to official Rust documentation/updateThis links to official Rust documentation semantics). Similarly to RefCell, it cannot be shared between threads safely.
  • MutexThis links to official Rust documentation/RwLockThis links to official Rust documentation, which are thread-safe versions of RefCell used for sharing mutable data between threads. Those have a bigger runtime borrow-checking overhead (the typical behavior is to wait for the other thread to finish using the shared resource), but have no limitations regarding threading.
  • The Cargo vec.svgqcell crate provides four alternatives to RefCell, each with trade-offs and strong points of their own.

References[edit | edit source]