The Rustonomicon

repository·master·Indexed 25 days ago

https://github.com/rust-lang/nomicon

A guide to the unsafe aspects of the Rust programming language, with a focus on memory management, concurrency, and the Foreign Function Interface (FFI). It covers critical topics such as aliasing and its impact on compiler optimizations, as well as practical implementation guides for atomics and interior mutability through the construction of Arc and Mutex types.

Tokens
44.6K
Snippets
93
Records
162
Agent score
81%

What's inside Rustonomicon

  1. Understand the purpose and scope of The Rustonomicon

    master

    The Rustonomicon is a guide focused on the 'Dark Arts' of Unsafe Rust. It is intended for developers who already have a strong grasp of basic systems programming and Rust and wish to understand the low-level details of the language.

    Key characteristics:

    • Focus: It describes how to use language pieces together and the issues that arise when doing so (e.g., combining references, destructors, and unwinding), rather than just detailing syntax.
    • Prerequisites: It assumes considerable prior knowledge of Rust and systems programming. It is recommended to read The Rust Programming Language first if you are not comfortable with these topics.
    • Relationship to The Reference: While The Reference details syntax and semantics, The Rustonomicon explains the practical implications and complexities of those semantics. If the two documents disagree, assume The Reference is correct.
    • Scope: Topics include (un)safety, unsafe primitives, creating safe abstractions, subtyping, variance, exception-safety, uninitialized memory, type punning, concurrency, FFI, and optimization.
    • Edition: Unless otherwise noted, all Rust code examples in this book use the Rust 2024 edition.
  2. What is `MaybeUninit` and when to use it

    master

    MaybeUninit<T> is a wrapper type used to handle memory that has not been fully initialized. It is the standard replacement for the deprecated std::mem::uninitialized.

    Key Properties:

    • Drop Behavior: Dropping a MaybeUninit<T> does nothing. This makes it safe to use assignment (=) to initialize its contents, as Rust won't try to drop the 'old' uninitialized value.
    • Memory Layout: For many containers (like arrays), MaybeUninit<T> has the same memory layout as T, allowing for transmute operations. However, this is not guaranteed for all containers (e.g., Option<MaybeUninit<bool>> does not have the same layout as Option<bool>).
    • Safety Requirement: You must ensure that every control path (including panics) initializes the value before it is dropped if the underlying type T has a destructor. If a panic occurs before initialization, MaybeUninit prevents a double-free but may result in a memory leak of the already-initialized parts.
  3. Logical uninitialization via Move semantics

    master

    When a value is moved out of a variable, that variable becomes logically uninitialized if the type does not implement the Copy trait.

    • Copy types: For types like i32, moving the value to a new variable does not invalidate the original variable.
    • Non-Copy types: For types like Box<T>, moving the value makes the original variable logically uninitialized. To reassign a value to that variable later, it must be declared as mutable (mut).
    fn main() {
        let x = 0;
        let y = Box::new(0);
        let z1 = x; // x is still valid because i32 is Copy
        let z2 = y; // y is now logically uninitialized because Box isn't Copy
    }
  4. Understanding Soundness and Non-locality in Unsafe Rust

    master

    In Rust, an unsafe block is considered sound if safe code cannot cause Undefined Behavior (UB) through it. A major challenge when working with unsafe is that safety is non-local: the correctness of an unsafe operation often depends on the state established by surrounding "safe" code.

    For example, using get_unchecked is sound if the bounds check is correct, but becomes unsound if the safe logic (like a comparison operator) is modified to allow an out-of-bounds index. This means you must audit not just the unsafe block, but the safe logic that maintains the invariants required by that block.

    fn index(idx: usize, arr: &[u8]) -> Option<u8> {
        if idx < arr.len() { // This safe check ensures the following unsafe block is sound
            unsafe {
                Some(*arr.get_unchecked(idx))
            }
        } else {
            None
        }
    }
  5. Design the memory layout for an Arc implementation

    master

    When implementing a thread-safe shared ownership type like Arc<T>, the layout must manage both the shared data and the reference count. Because the reference count is shared mutable state, it should be stored in the same heap allocation as the data to avoid extra indirection.

    To implement this correctly in Rust, a naive structure using raw pointers is insufficient due to strict variance and incorrect ownership information provided to the drop checker. A correct implementation requires:

    1. NonNull<T>: Used instead of a raw pointer to ensure the pointer is never null and to provide covariance over T (allowing Arc<&'static str> to be used where Arc<&'a str> is expected).
    2. PhantomData<T>: Used to inform the compiler/drop checker that the structure has an ownership relationship with the inner data, ensuring proper destruction when the last owner is dropped.

    The final layout consists of an Arc<T> struct holding a NonNull pointer to an ArcInner<T> and a PhantomData marker, while ArcInner<T> contains the AtomicUsize reference count and the actual data T.

    use std::marker::PhantomData;
    use std::ptr::NonNull;
    use std::sync::atomic::AtomicUsize;
    
    pub struct Arc<T> {
        ptr: NonNull<ArcInner<T>>,
        phantom: PhantomData<ArcInner<T>>,
    }
    
    pub struct ArcInner<T> {
        rc: AtomicUsize,
        data: T,
    }
  6. How the dot operator performs method lookup

    master

    The dot operator (.) in Rust performs automatic type conversions to match a method's receiver type. When you call value.foo(), where value has type T, the compiler follows a specific lookup algorithm to find the correct implementation of foo:

    1. By Value: The compiler first tries to call T::foo(value) directly.
    2. Autoref: If by-value fails, the compiler attempts to add an automatic reference, trying <&T>::foo(value) and <&mut T>::foo(value).
    3. Dereferencing: If autoref fails, the compiler uses the Deref trait to dereference T. If T: Deref<Target = U>, it tries again with type U.
    4. Unsizing: If dereferencing fails, the compiler may try "unsizing" T. This converts a type with a known compile-time size into a slice type (e.g., converting [i32; 2] into [i32]) to resolve methods.

    This process allows complex chains of indirections, such as accessing an index on an Rc<Box<[T; 3]>>, to work by traversing through Rc dereferencing, Box dereferencing, and finally unsizing the array into a slice.

    let array: Rc<Box<[T; 3]>> = ...;
    let first_entry = array[0];
  7. Understand and use Dynamically Sized Types (DSTs)

    master

    Dynamically Sized Types (DSTs) are types without a statically known size or alignment. Because they lack a known size, they cannot be stored directly on the stack; they can only exist behind a pointer. These pointers are "wide pointers" that include the address plus additional metadata required to complete the type information.

    Major DSTs in Rust include:

    • Trait objects: dyn MyTrait. The pointer includes a vtable pointer for runtime reflection. The runtime size can be requested from the vtable.
    • Slices: [T], str, etc. The pointer includes the number of elements. The runtime size is size_of::<T>() * number_of_elements.

    You can define a struct containing a DST as its last field, but that struct itself becomes a DST. To create custom DSTs, use a generic type and perform an unsizing coercion.

    // Can't be stored on the stack directly
    struct MySuperSlice {
        info: u32,
        data: [u8],
    }
    
    struct MySuperSliceable<T: ?Sized> {
        info: u32,
        data: T,
    }
    
    fn main() {
        let sized: MySuperSliceable<[u8; 8]> = MySuperSliceable {
            info: 17,
            data: [0; 8],
        };
    
        let dynamic: &MySuperSliceable<[u8]> = &sized;
    
        // prints: "17 [0, 0, 0, 0, 0, 0, 0, 0]"
        println!("{} {:?}", dynamic.info, &dynamic.data);
    }
  8. Designing unsafe traits

    master

    When designing your own traits, deciding whether to mark them unsafe is a critical API choice.

    • Safe Traits: Easier for users to implement, but any unsafe code that relies on the trait must include defensive logic to handle incorrect implementations.
    • Unsafe Traits: Shifts the responsibility of upholding contracts to the implementor. Use an unsafe trait if the unsafe code consuming the trait cannot reasonably defend against a broken implementation (e.g., thread safety or memory allocation logic).

    Example of a hypothetical unsafe trait for total ordering:

    use std::cmp::Ordering;
    
    unsafe trait UnsafeOrd {
        fn cmp(&self, other: &Self) -> Ordering;
    }
  9. How IntoIter consumes a Vec by-value

    master
    Unlike iter and iter_mut which work on slices, into_iter consumes the Vec<T> by-value. This allows the iterator to yield its elements by-value (moving them out of the collection). To achieve this, the IntoIter struct must take control of the Vec's allocation to ensure the memory is managed correctly and can be freed once iteration is complete.
  10. Understand Ownership Based Resource Management (OBRM/RAII)

    master

    Ownership Based Resource Management (OBRM), also known as RAII (Resource Acquisition Is Initialization), is a core pattern in Rust used to manage system resources.

    The Pattern:

    1. Acquisition: To acquire a resource, you create an object that manages it.
    2. Release: To release the resource, you destroy the object, which triggers automatic cleanup.

    Commonly Managed Resources:

    • Memory: Managed via types like Box, Rc, and most collections in std::collections. This is critical in Rust because it lacks a pervasive Garbage Collector (GC).
    • System Resources: Other resources such as threads, files, or sockets are typically exposed through similar OBRM-style APIs.
  11. Prevent pointer invalidation via reference freezing

    master

    Rust prevents bugs where a collection is modified (e.g., reallocated) while a reference to its internal elements is still active. When a reference is created, Rust requires that the referent and its owners are 'frozen' to prevent mutations that would invalidate the reference.

    let mut data = vec![1, 2, 3];
    // get an internal reference
    let x = &data[0];
    
    // OH NO! `push` causes the backing storage of `data` to be reallocated.
    // Dangling pointer! Use after free! Alas!
    // (this does not compile in Rust)
    data.push(4);
    
    println!("{}", x);
  12. Understanding resource leaks and `mem::forget`

    master

    While Safe Rust makes it difficult to leak resources in an uncontrolled way, it does not prevent all forms of leaking.

    One specific way to leak a value is using std::mem::forget. This function consumes the value passed to it and prevents its destructor from running.

    While leaking a simple type like Box<u8> only results in a memory leak, leaking proxy types (types that manage access to a distinct object without owning it) can lead to Und Undefined Behavior (UB). Unsafe code must not rely on destructors being called to maintain memory safety, as mem::forget can bypass them.