Raw Pointers at FFI Boundaries
At an FFI boundary, C pointers (const T* and T*) map to Rust raw pointers (*const T and *mut T) or to non-null pointer types (NonNull<T>, Option<&T>, Option<&mut T>). Unlike Rust references (&T and &mut T), raw pointers may be null (std::ptr::null()), unaligned, uninitialized, or aliased, so validating raw pointers before converting them into Rust references or slices is critical for FFI soundness.
C Caller Passes: (const uint8_t *ptr, size_t len)
|
v
Rust FFI Boundary Checks:
1. Is `len == 0`? ---> Return `&[]` immediately!
(Never pass NULL to `slice::from_raw_parts`, even with len 0!)
2. Is `ptr.is_null()`? ---> Return error code (e.g., `-1`)
3. Is `ptr.is_aligned()`? ---> Required for `T` with `align_of::<T>() > 1`
4. Construct slice safely ---> `unsafe { std::slice::from_raw_parts(ptr, len) }`
Why Fat Pointers (&[T] and &str) Cannot Cross extern "C" Directly
In Rust, a slice reference &[T] or string slice &str is a 16-byte fat pointer (on 64-bit platforms) containing (data_ptr: *const T, len: usize) laid out with the unstable #[repr(Rust)] representation. C has no built-in equivalent to a Rust fat pointer.
Always decompose a slice into two separate parameters across extern "C": a thin raw pointer (*const T or *mut T) and a length (usize):
| Rust Internal Type | FFI Parameter Signature (extern "C") |
Conversion Method |
|---|---|---|
&[T] |
ptr: *const T, len: usize |
slice.as_ptr(), slice.len() -> slice::from_raw_parts(ptr, len) |
&mut [T] |
ptr: *mut T, len: usize |
slice.as_mut_ptr(), slice.len() -> slice::from_raw_parts_mut(ptr, len) |
Option<&T> |
Option<&T> (or *const T) |
Niche optimization: Option<&T> is guaranteed to have the exact same ABI as a nullable C pointer const T* (None == NULL)! |
Option<NonNull<T>> |
Option<NonNull<T>> (or *mut T) |
Guaranteed to have the exact same layout and ABI as *mut T (None == NULL). |
Critical Rule: slice::from_raw_parts(NULL, 0) Is Undefined Behavior!
In C, it is common to pass (NULL, 0) to represent an empty buffer. However, in Rust, std::slice::from_raw_parts(ptr, len) requires ptr to be non-null and aligned even when len == 0! Passing (std::ptr::null(), 0) to slice::from_raw_parts is immediate Undefined Behavior (and caught by Miri).
Always handle len == 0 before calling slice::from_raw_parts:
/// Safely converts a C `(ptr, len)` buffer pair into `Option<&'a [T]>`.
///
/// - Returns `Some(&[])` if `len == 0` (even if `ptr` is null!).
/// - Returns `None` if `len > 0` and `ptr` is null or misaligned.
///
/// # Safety
///
/// If `len > 0` and `ptr` is non-null and aligned, `ptr` must point to `len`
/// contiguous initialized elements of `T` that remain valid and unmutated for `'a`.
pub unsafe fn slice_from_c_parts<'a, T>(ptr: *const T, len: usize) -> Option<&'a [T]> {
if len == 0 {
return Some(&[]);
}
if ptr.is_null() || !ptr.is_aligned() {
return None;
}
// SAFETY: `len > 0`, `ptr` is non-null and properly aligned for `T`,
// and caller guarantees `len` initialized elements.
Some(unsafe { std::slice::from_raw_parts(ptr, len) })
}
/// Outbound C function accepting an out-parameter via nullable `Option<&mut u64>`.
#[unsafe(no_mangle)]
pub unsafe extern "C" fn sum_u32_buffer(
ptr: *const u32,
len: usize,
out_sum: Option<&mut u64>,
) -> i32 {
let Some(out) = out_sum else {
return -1; // `out_sum` was NULL
};
// SAFETY: Caller guarantees `(ptr, len)` is a valid buffer if non-null.
let Some(slice) = (unsafe { slice_from_c_parts(ptr, len) }) else {
return -2; // `ptr` was NULL or misaligned with `len > 0`
};
*out = slice.iter().copied().map(u64::from).sum();
0
}
fn main() {
let mut total = 0u64;
// 1. Passing `(NULL, 0)` succeeds safely and produces 0:
let rc_empty = unsafe { sum_u32_buffer(std::ptr::null(), 0, Some(&mut total)) };
assert_eq!((rc_empty, total), (0, 0));
// 2. Passing a real slice:
let nums = [10u32, 20, 30, 40];
let rc_ok = unsafe { sum_u32_buffer(nums.as_ptr(), nums.len(), Some(&mut total)) };
assert_eq!((rc_ok, total), (0, 100));
println!("Safe C buffer sum: rc={rc_ok}, total={total}");
}
Safe C buffer sum: rc=0, total=100