How it works
The explanation, with examples you can run.
The five operations
- Dereference a raw pointer (
*const Tor*mut T). - Call an
unsafe fn, including foreign functions. - Read or write a
static mut. - Implement an
unsafe trait, such asSendorSyncby hand. - Read a field of a
union.
Everything else is still checked inside an unsafe block: types, the borrow rules for references, bounds checks on indexing. Writing unsafe doesn't make code faster by itself.
A safe API over unsafe code
unsafe extern "C" { // edition 2024: extern blocks must be marked unsafe safe fn abs(x: i32) -> i32; // we promise this signature is right, so callers need no unsafe fn strlen(s: *const std::ffi::c_char) -> usize;} fn split_at_mut(v: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { assert!(mid <= v.len()); let len = v.len(); let p = v.as_mut_ptr(); // SAFETY: the two halves don't overlap and both stay inside v. unsafe { ( std::slice::from_raw_parts_mut(p, mid), std::slice::from_raw_parts_mut(p.add(mid), len - mid), ) }} fn main() { let mut nums = [1, 2, 3, 4, 5]; let (a, b) = split_at_mut(&mut nums, 2); a[0] = 10; b[0] = 30; println!("{nums:?}"); let x = 7; let p = &x as *const i32; // making a raw pointer is safe let y = unsafe { *p }; // dereferencing one is not let n = unsafe { strlen(c"hello".as_ptr()) }; // c"..." literals: Rust 1.77+ println!("{y} {} {n}", abs(-3));}It prints [10, 2, 30, 4, 5] and 7 3 5. The borrow checker can't see that two mutable slices of one array don't overlap, so split_at_mut (a simplified copy of the standard library's method) proves it with an assert! and then builds the slices from raw pointers. Callers get an ordinary safe function.
What counts as undefined behaviour
The Reference lists it. The main items: dereferencing a dangling, null or misaligned pointer; a data race; breaking the aliasing rules (for example a &mut T and another live reference to the same value); producing an invalid value, such as a bool that is not 0 or 1 or a char outside Unicode; and reading uninitialised memory. The compiler optimises on the assumption that these never happen, so their effects can show up far from the bug. Tools help: cargo miri test (on nightly) interprets your tests and reports many kinds of undefined behaviour.
FFI
Calling C means declaring its functions in an extern "C" block. In the 2024 edition the block must be written unsafe extern, since you are vouching for the signatures, and each item can be marked safe or unsafe. Use the types in std::ffi (c_char, CStr, CString) and #[repr(C)] structs so layouts match. Exporting Rust functions to C uses #[unsafe(no_mangle)] in 2024 and extern "C" fn. A panic must not unwind across a plain extern "C" boundary; Rust aborts the process if one tries. Tools such as bindgen generate declarations from C headers.
Also new in 2024: unsafe_op_in_unsafe_fn warns by default, so even inside an unsafe fn each unsafe operation goes in its own unsafe { } block with its own justification.
Check yourself
3 questions. Get them all right to finish this topic.
0 of 3 right so far. Get all three to finish this topic.