25.2 Unsafe Blocks and Functions
Operations designated as unsafe can only be performed within contexts explicitly marked by the unsafe
keyword.
25.2.1 Unsafe Blocks
An unsafe { ... }
block isolates a segment of code containing one or more unsafe operations. This is the most common way to introduce unsafety. It signals that the code within the block might perform actions requiring manual safety verification.
A frequent use case is dereferencing raw pointers. While creating, passing, or comparing raw pointers is safe, reading from or writing to the memory they point to (*ptr
) requires an unsafe
block. This is because the compiler cannot guarantee that the pointer is valid (i.e., pointing to allocated, initialized, and properly aligned memory of the correct type).
fn main() { let mut num: i32 = 42; // Creating a raw pointer from a valid reference is safe. let r_ptr: *mut i32 = &mut num; // Dereferencing the raw pointer requires an unsafe block. unsafe { println!("Value before: {}", *r_ptr); // Modify the value through the raw pointer. *r_ptr = 99; println!("Value after: {}", *r_ptr); } // The original variable reflects the change. println!("Final value of num: {}", num); // num is now 99 }
In this example, the operation is safe because r_ptr
originates from a valid mutable reference &mut num
. The unsafe
block serves as an annotation that the programmer, not the compiler, is responsible for ensuring this validity.
25.2.2 Unsafe Functions
A function can be declared as unsafe fn
if calling it requires the caller to satisfy certain preconditions (invariants) that the compiler cannot enforce through the type system or borrow checker alone. Such functions can perform unsafe operations internally without needing additional unsafe
blocks for those specific operations.
However, calling an unsafe fn
is itself an unsafe operation and must occur within an unsafe
block or another unsafe fn
.
// This function is unsafe because dereferencing `ptr` is only valid // if the caller guarantees `ptr` points to valid, initialized memory. unsafe fn read_from_pointer(ptr: *const i32) -> i32 { unsafe {//Explicit unsafe block for the dereference (recommended by Rust 2024 lint) *ptr } } fn main() { let x = 42; let ptr = &x as *const i32; // Calling an unsafe function requires an unsafe block. let value = unsafe { read_from_pointer(ptr) }; println!("Value read via unsafe fn: {}", value); }
The unsafe
keyword on the function signature acts as a contract: “Warning: This function relies on preconditions not checked by the compiler. Incorrect usage can lead to undefined behavior. Ensure you meet its documented requirements before calling.”
25.2.3 unsafe fn
and Explicit unsafe
Blocks in Rust 2024
In previous editions, an unsafe fn
implicitly permitted unsafe operations within its body without additional unsafe { ... }
blocks. The unsafe
keyword on the function served two roles: declaring that calling the function requires unsafe
, and allowing unsafe
operations inside.
With the Rust 2024 Edition, the unsafe_op_in_unsafe_fn
lint now warns by default if unsafe
operations are performed directly within an unsafe fn
without being enclosed in an explicit unsafe { ... }
block. This change helps protect against accidental unsafe usage and encourages minimizing the scope of unsafe
operations, making it clearer exactly where the programmer is taking responsibility.
Consider this example:
// An unsafe function that performs an unchecked slice access. // The `unsafe` keyword on the function means callers need an `unsafe` block. unsafe fn get_unchecked_val<T>(slice: &[T], index: usize) -> &T { // In Rust 2024, the `unsafe_op_in_unsafe_fn` lint will now warn // if `slice.get_unchecked(index)` is not wrapped in an `unsafe` block here. unsafe { // Explicit unsafe block for the unchecked access. slice.get_unchecked(index) } } fn main() { let data = vec![10, 20, 30]; let index = 1; let value = unsafe { // Calling the `unsafe fn` requires an `unsafe` block. get_unchecked_val(&data, index) }; println!("Value at index {}: {}", index, value); // Outputs 20 // Attempting to use a potentially invalid index: let out_of_bounds_index = 5; // unsafe { // // This call will likely lead to Undefined Behavior if actually run, // // as `get_unchecked_val` doesn't check `index`. // let _ = get_unchecked_val(&data, out_of_bounds_index); // } }
This change means that while an unsafe fn
allows unsafe operations, it is now best practice (and warned against if not followed) to still use explicit unsafe { ... }
blocks within unsafe fn
bodies to precisely demarcate the code sections where the safety invariants must be manually upheld.
25.2.4 Choosing between unsafe fn
and unsafe
Block
Choosing between an unsafe fn
and an unsafe
block inside a safe function depends on where the responsibility for safety lies:
- Use
unsafe fn
when the function has preconditions that the caller must fulfill to ensure safety. Violating these preconditions, even if the function call type-checks, could lead to UB. Safety depends on the caller’s context. - Use an
unsafe
block inside a safe function (fn
) when the function itself can guarantee that its internal unsafe operations are performed correctly, provided the function is called with arguments valid according to its safe signature. Safety is maintained by the function’s implementation.
Best Practice: Encapsulate unsafe operations within unsafe
blocks inside safe functions whenever feasible. This minimizes the surface area of unsafety and presents a safe interface to the rest of the codebase. Reserve unsafe fn
for interfaces where safety fundamentally depends on guarantees provided by the caller, often seen in FFI or low-level abstractions.