Raw pointers allow expressing states that cannot be directly represented in Rust's reference model, such as nullable external handles, uninitialized storage within containers, low-level allocation, and hardware interfaces. FFI is a significant use case, but not the only one.
*const T and *mut T do not carry lifetimes that the borrow checker can fully track. They may be null or misaligned; you must satisfy all requirements for the specific access operation only when you actually perform it.
Pointer Existence and Access Validity Are Different
fnmain(){let value =42_u32;let pointer =&raw const value;// SAFETY: value is still alive and initialized; pointer is aligned, and the read has no competing writes.
let copied =unsafe{ pointer.read()};assert_eq!(copied,42);let null =std::ptr::null::<u32>();assert!(null.is_null());// Saving and checking for a null pointer does not require dereferencing
}
Multiple *mut T pointers can point to the same location, but this does not authorize you to create simultaneously valid and mutually conflicting &mut T references, or to perform non-atomic concurrent reads and writes. The const/mut on a pointer indicates the direction of access allowed through that type; it does not automatically make an object that was originally read-only or protected by shared references writable. Pointer Module
ptr.read on non-Copy types bitwise reads out an owned value; the caller must avoid subsequently freeing the same resource as two independent owners. ptr.write does not destruct the original value first; whether to release existing resources before overwriting must be handled separately.
Converting Raw Pointers to References Adds Obligations
&*ptr or &mut *ptr is not just "a more convenient syntax." The new reference requires a non-null, aligned, valid target value, and the corresponding access constraints for the entire borrow period. Even if there is no immediate read or write afterwards, these requirements cannot be ignored.
Similarly, when the length of slice::from_raw_parts is zero, it usually still requires a non-null and aligned pointer; a C interface allowing NULL+0 does not mean you can directly construct a Rust empty slice. External buffers must first verify the requirements of their respective interfaces.
Safe wrappers returning references should also bind the lifetime to the object that actually holds the storage. You cannot give callers an arbitrarily long borrow based on a raw pointer that has no owner information. This is more important than simply marking a function body with unsafe.
Branch for NULL + 0 Separately, But Don't Treat Null Checks as Complete Validation
The following function calculates a byte-wise XOR checksum, allowing empty buffers expressed as NULL + 0. It uses unsafe fn because memory validity for non-null pointers cannot be proven solely by integer checks.
/// # Safety
/// When len > 0 and data is non-null, the caller guarantees it points to len initialized bytes within the same allocation,
/// readable and alive for the entire call duration, with no conflicting writes; length does not exceed isize::MAX.
unsafefnxor_bytes(data: *const u8, len:usize)->Result<u8, &'staticstr>{if len ==0{returnOk(0);}if data.is_null(){returnErr("Non-zero length corresponds to a null pointer");}if len >isize::MAXasusize{returnErr("Length too large");}// SAFETY: Null pointer and excessive length are excluded above; remaining range, initialization, and access prerequisites are guaranteed by the caller.
let bytes =unsafe{std::slice::from_raw_parts(data, len)};Ok(bytes.iter().fold(0,|result,byte|result ^ byte))}fnmain(){let bytes =[1u8,2,3,4];// SAFETY: The array remains valid, pointer and length come from the same complete array, no concurrent writes.
assert_eq!(unsafe{xor_bytes(bytes.as_ptr(), bytes.len())},Ok(4));// SAFETY: These two combinations are explicitly handled by the interface and do not access null pointers.
assert_eq!(unsafe{xor_bytes(std::ptr::null(),0)},Ok(0));assert!(unsafe{xor_bytes(std::ptr::null(),1)}.is_err());}
After checking that data is non-null and len has not overflowed, you still don't know how large the buffer actually is, nor whether it has been freed; therefore, you cannot change the function into a pointer validator that can be called safely arbitrarily. The len in from_raw_parts counts elements, not bytes; when switching to u32, you must also consider alignment and total byte size. from_raw_parts
If the upper layer already holds a &[u8], just accept the slice directly; you only need to maintain these additional prerequisites when truly crossing boundaries lacking Rust lifetime information. Don't split a reliable slice into pointers just to demonstrate unsafe, only to guess the length back in another place.
Provenance: Integer Address Equality Is Not Interchangeable
Pointers carry semantics beyond just address, such as their access source. Integer round-tripping, out-of-bounds pointer arithmetic, and stitching across allocations can all break operation prerequisites. Even if two allocations happen to be adjacent, they cannot automatically be synthesized into a Rust slice.
Operations like add/offset have range and overflow requirements; wrapping_add can relax conditions during calculation, but it does not make the final dereference legal. When adjusting addresses, understanding interfaces like addr and with_addr that preserve provenance is easier to express intent than arbitrary as usize conversions back and forth. Rust's complete aliasing model is still evolving; rely on explicit standard library contracts rather than treating a specific interpreter model as the final specification for all details.
Overlapping Copy and Non-Overlapping Copy Are Different Contracts
fnmain(){letmut bytes =[1u8,2,3,4,5];let pointer = bytes.as_mut_ptr();// SAFETY: Source 0..4 and destination 1..5 are both within the same valid array; u8 is aligned, overlapping is allowed.
unsafe{std::ptr::copy(pointer, pointer.add(1),4);}assert_eq!(bytes,[1,1,2,3,4]);letmut safe =[1u8,2,3,4,5];
safe.copy_within(0..4,1);assert_eq!(safe, bytes);}
ptr::copy semantically allows overlapping, behaving as if saving the source region first and then writing to the destination; you cannot replace it with copy_nonoverlapping just because a test passed. The count for both is the number of elements of type T. For regular slice data, prefer interfaces like copy_within and copy_from_slice that check boundaries. ptr::copy
Bitwise copying is also not calling Clone. For types owning resources like String, copying the pointer/length fields and letting both objects destruct would destroy ownership; if containers use this operation to move elements internally, they must synchronously maintain which slots are still considered to validly own values. The u8 example avoids this destruct responsibility, but that doesn't mean generalizing to arbitrary T is equally simple.
Copy direction cannot replace a non-overlap contract
Move the destination from disjoint to overlapping. Results follow ptr::copy semantics, always using original source values. A handwritten loop direction that happens to work cannot change copy_nonoverlapping’s caller obligations.
Preparing the visual
Copy direction cannot replace a non-overlap contract
Packed: Allows Unaligned Layout, Does Not Allow Unaligned References
#[repr(C, packed)]structPacket{kind:u8,
length:u32,
}fnmain(){let packet = Packet { kind:1, length:42};let pointer =&raw const packet.length;// SAFETY: Points to an initialized u32, stored validly during access; uses unaligned read.
let length =unsafe{ pointer.read_unaligned()};assert_eq!(length,42);}
Do not write &packet.length first and then cast it to a raw pointer; that already attempts to form an unaligned reference. repr(packed) also does not handle endianness; this memory layout does not mean it is designed for cross-platform network encoding. For byte protocols, it is often better to use from_le_bytes/from_be_bytes to avoid relying on struct memory layout.
Three Boundaries of FFI
extern "C" determines the target platform's C calling convention, repr(C) determines the layout rules for corresponding types, and specific pointer/length/release protocols determine resource semantics. The C ABI for x86-64 Unix and Windows are not the same; you cannot generalize the System V register table to all x86-64.
Rust 2024's external function declarations use unsafe extern blocks; exporting fixed names uses #[unsafe(no_mangle)], while still requiring correct extern "C" function types. Integer widths use target aliases from core::ffi etc. to match; do not permanently bind all C int/long to the same Rust width.
The actual offsets of repr(C) structs are influenced by the target ABI and the layout of the fields themselves; an outer repr(C) does not recursively correct inner default Rust types. Valid values for enums and bool across boundaries must also match; you cannot assume safety just because sizeof is the same. Type Layout
C Strings: Check Terminator, Encoding, and Ownership Separately
usestd::ffi::{CStr, CString};fnmain(){let name =CString::new("hello").unwrap();let pointer = name.as_ptr();// SAFETY: pointer is borrowed from a still-alive CString, NUL is within the same allocation, no writes during the period.
let borrowed =unsafe{CStr::from_ptr(pointer)};assert_eq!(borrowed.to_str().unwrap(),"hello");assert!(CString::new(b"a\0b".to_vec()).is_err());let non_utf8 =CStr::from_bytes_with_nul(b"\xff\0").unwrap();assert!(non_utf8.to_str().is_err());assert_eq!(non_utf8.to_bytes(),&[0xff]);}
CString guarantees its own NUL-terminated layout, but does not guarantee the content is UTF-8; CStr can borrow valid non-UTF8 byte strings, so converting to &str still requires validation via to_str. CStr::from_ptr searches for the terminator; the caller must guarantee it is readable within valid storage. If external code provides length, prefer bounded buffer interfaces over unbounded scanning.
Do not write CString::new(...).unwrap().as_ptr() and then keep the pointer for the next statement: the temporary CString might already be freed. Even if you save the CString in a variable, if the external library continues to hold this pointer after the function returns, you must extend the lifetime of the true owner and coordinate the final external access. CString
After Transferring Ownership, You Can Only Retrieve It According to the Original Protocol
usestd::ffi::CString;fnmain(){let raw =CString::new("owned").unwrap().into_raw();// Here you can hand raw to external code as agreed; the example does not modify its content or length.
// SAFETY: raw comes from the above into_raw, has not been reclaimed, and the terminator position has not changed.
let restored =unsafe{CString::from_raw(raw)};assert_eq!(restored.to_str().unwrap(),"owned");}
from_raw is not "turning any C string into a Rust owner." It only takes back the CString that matches the one exported by into_raw; if external code changes the terminator position, the recalculated length might be inconsistent with the original allocation, violating the reclamation contract. Strings allocated by external libraries should use the release function they provide; if necessary, copy them into Rust-owned data first.
Source
Common Correct Release Responsibility
Cannot Directly Replace With
Box::into_raw
Box::from_raw once according to original type and allocation contract
libc free, repeated from_raw
CString::into_raw
CString::from_raw once after meeting length requirements etc.
Arbitrary C string pointers
Handle returned by external create
Same library destroy/close
Default Rust allocator release
Borrow-only as_ptr
True owner releases after borrow ends
Creating a second owner from a borrow pointer
What Work Remains After Binding Generation
bindgen generates declarations from header files; output should be placed in OUT_DIR, consistent with include! paths. It does not automatically discover whether C saves pointers, whether callback threads are concurrent, or who is responsible for freeing a certain return value.
See C ABI and bindgen for complete, runnable C/Rust bindings and resource protocols. The division of labor between the two articles is: this one explains the validity conditions for a single pointer access, and the other explains how to wrap external library contracts into safe interfaces.
Callbacks and Unwinding Are Judged by Specific ABI
Rust panics crossing ordinary non-unwind C ABI boundaries will cause process termination; external exceptions entering Rust boundaries that do not allow unwinding may be undefined behavior. C-unwind provides different conventions for scenarios allowing unwinding; this cannot be attributed to "C stacks definitely having no unwind info."
Using catch_unwind inside callbacks only handles Rust panics that can unwind; it does not catch aborts, nor can it automatically restore data structures interrupted during updates. Whether it is allowed to continue callbacks after deregistration, or whether you must wait for in-flight calls to finish, also belongs to FFI safety conditions. FFI Unwinding
Draw the Last Access for Callbacks
When a registration function receives callback and user_data, you must at least know whether the callback can be concurrent, on which threads it runs, whether deregistration waits for in-flight calls, and whether user_data is still retained if it fails. Only after the external library guarantees no further callback access will Rust be able to release the pointed-to object.
A common safe wrapper involves the owner saving the external handle, deregistering in Drop, and completing necessary in-flight call coordination; if deregistration might synchronously trigger callbacks, you must also avoid holding locks needed by the callback. If the library's destruction protocol requires asynchronous completion, synchronous Drop alone may be insufficient; you need an explicit close interface and define behavior when discarded without closing.
These constraints come from the real contracts of external libraries and cannot be deduced from the signature extern "C" fn(*mut c_void). Type declarations solve calling conventions; safe wrappers are responsible for completing the resource timeline.