Unsafe and FFI

Unsafe is a checked promise at a small boundary. FFI crosses that boundary with repr(C), CString, and documented ownership.

Unsafe and FFI

Everything before this lesson is safe Rust: if it compiles, those memory rules hold. Unsafe is a small list of extra operations, and only after you can state why they are still sound. Foreign functions are the second section, because they are the usual reason to write that block.

Unsafe

unsafe does not switch off the borrow checker. It allows five things you must uphold yourself.

OperationYou must guarantee
Dereference a raw pointerThe pointer is valid, aligned, and the borrow rules still hold
Call an unsafe functionYou meet that function’s safety contract
Read or write a mutable staticNo data race on that address
Implement an unsafe trait, such as Send, by handThe type really has the property the trait claims
Read a union fieldYou read the field that was last written

Production code reaches for unsafe in these places. Undefined behavior means the compiler may assume that path never happens. The symptom is not a tidy panic.

PlaceWhy
A foreign-function boundaryThe other language does not follow Rust’s rules
A structure you measured as too slowOnly after a measurement, not a guess
The operating systemThe call is specified outside safe Rust
A type you know is SendThe compiler cannot see the reason itself
safe functionchecks lengthunsafe blockraw read
StatusThe safe shell rejects the bad call

Callers of a safe function must not be able to cause undefined behavior. The check belongs outside the block.

01_unsafe_basics.rsRust
fn first_unchecked(xs: &[i32]) -> Option<i32> {
    if xs.is_empty() {
        return None;
    }
    Some(unsafe {
        // SAFETY: length was checked above, so index 0 is in range.
        *xs.get_unchecked(0)
    })
}

fn main() {
    println!("{:?}", first_unchecked(&[4, 5]));
}

Foreign functions

extern "C" declares a function another language can call, or one you call. The layout must be repr(C) so field order matches C. A Rust String is not a C string. A CString owns the bytes and the trailing zero. Who frees the pointer has to be written down: if C allocated it, C’s free releases it. bindgen generates the declarations. no_mangle keeps the symbol name. A panic that unwinds across the foreign boundary is undefined behavior. Catch it at the edge, or abort.

safe Rustchecksunsafeextern CCforeign
StatusConvert on the Rust side

Null pointers, lengths, and UTF-8 are checked before the call. The unsafe block is the call itself.

02_ffi_basics.rsRust
fn main() {
    println!("extern C and repr(C) are the boundary, not a second language");
    println!("the safe side checks pointers; the unsafe side calls across");
    println!("a panic must not unwind through foreign code");
}