6 min read #rust #unsafe-rust
On this page

Raw Pointers and FFI

*const T and *mut T are not constrained by the borrow checker—they can be aliased freely and may not point to valid memory. Their sole purpose is FFI: calling C libraries, receiving C callbacks, and manipulating hardware registers. An unsafe block does not "disable safety checks"; rather, it shifts safety responsibility from the compiler to the programmer.

Raw Pointers: Pointers Without a Borrow Checker

let x = 42;
let ptr: *const i32 = &x;            // Reference → Raw Pointer (safe)
unsafe {
    println!("{}", *ptr);            // Dereference (unsafe — compiler does not validate validity)
}

Raw pointers do not need to adhere to borrow checker rules—you can have multiple *mut T pointing to the same data, they can be null, and alignment is not guaranteed. All responsibility shifts to the programmer.

Difference from C int *: Rust distinguishes between *const T and *mut T (similar to C's const int * vs int *). However, C can implicitly discard const, whereas Rust cannot.

extern "C": C Function Interface

extern "C" {
    fn abs(input: i32) -> i32;       // Declare C function
}

#[no_mangle]                         // Prevent Rust name mangling
pub extern "C" fn rust_callback(x: i32) -> i32 {
    x + 1                            // C code can call this function
}

extern "C" uses the C calling convention: arguments are arranged in registers/stack according to the target platform's C ABI. On x86-64, this is the System V AMD64 ABI (arguments passed via rdi, rsi, rdx, rcx, r8, r9). You can also use extern "system" (Windows API) and extern "Rust" (default).

#[repr(C)]: C-Compatible Memory Layout

#[repr(C)]
struct MyStruct {
    a: i32,                          // offset 0
    // padding: 4 bytes               // C requires f64 to be aligned to 8
    b: f64,                          // offset 8
}                                    // size: 16 bytes

Without #[repr(C)], the Rust compiler may reorder fields to optimize struct size. With #[repr(C)], fields are ordered as declared, with padding added according to C ABI requirements—ensuring binary compatibility with C structs. #[repr(C, packed)] eliminates all padding (but may create unaligned references—unsafe).

bindgen: Automatically Generate Rust FFI Bindings

Generate Rust extern declarations, struct definitions, and constants from C headers:

// build.rs:
fn main() {
    println!("cargo:rerun-if-changed=wrapper.h");
    let bindings = bindgen::Builder::default()
        .header("wrapper.h")
        .allowlist_function("my_lib_.*")   // Only bind these functions
        .generate().expect("bindgen failed");
    bindings.write_to_file("src/bindings.rs").unwrap();
}
// src/lib.rs:
include!(concat!(env!("OUT_DIR"), "/bindings.rs"));

Callbacks: The Trap of C Calling Rust

extern "C" fn callback(x: i32) {
    println!("C called with {}", x);
}
unsafe { register_callback(callback as unsafe extern "C" fn(i32)); }

When passing Rust functions as callbacks to C, panics cannot cross FFI boundaries—if a panic occurs within the callback and unwinding crosses the C stack frame, this is UB (C stacks do not have unwind tables). Solutions: catch panics within the callback using std::panic::catch_unwind, or set panic = "abort" in Cargo.toml.

References

  • Rustonomicon: FFI, C ABI
  • bindgen: rust-lang.github.io/rust-bindgen
  • The Embedded Rust Book: FFI and C interaction

*Keywords: raw pointer, *const, mut, extern "C", #[no_mangle], repr(C), bindgen, FFI callback, C ABI