Slices in FFI
C has no slice type. A function that works on an array takes a pointer to the first element and, if you’re lucky, the number of elements as a separate argument:
int sum(const int *items, size_t len);
Rust’s slices are the same pair of pointer and length, packed into one value
with guarantees attached. A &[i32] always points to len valid, initialized
i32s, and the borrow checker makes sure they stay that way while you use them.
Turning C’s pair into a slice is one of the most common operations in FFI code,
and one of the easiest to get subtly wrong.
Slices from C
std::slice::from_raw_parts turns a pointer and a length into a slice, and
from_raw_parts_mut does the same for a mutable one. The
contract is longer
than most people expect:
- The pointer must be non-null and aligned, even when the length is 0.
- It must point to
lenconsecutive, initialized values of typeT, all within a single allocation. - The memory must not be mutated while the slice is in use. For
from_raw_parts_mut, it must not be accessed through any other pointer at all. - The total size,
len * size_of::<T>(), must be at mostisize::MAXbytes. - The lifetime of the slice is whatever the caller picks, so you have to tie it to something that keeps the memory alive.
The first rule is the one that catches people. In C, passing NULL with a
length of 0 is a normal way to say “no items”. For from_raw_parts, it’s
undefined behavior, because a slice’s pointer must never be null, not even an
empty one. So handle the empty case before you build a slice:
use std::slice;
/// # Safety
///
/// If `len` is non-zero, `ptr` must be non-null and aligned, and point to `len`
/// initialized `T`s that stay alive and unmodified for `'a`.
pub unsafe fn slice_from_c<'a, T>(ptr: *const T, len: usize) -> &'a [T] {
if len == 0 {
return &[];
}
// SAFETY: `len` is non-zero, so the caller guarantees the whole contract.
unsafe { slice::from_raw_parts(ptr, len) }
}
Everything else stays the caller’s responsibility. If C says 10 and allocated 8,
you read past the end, and the # Safety section is the place that says whose
job it was to prevent it.
Debug builds check some of these rules at runtime and abort with
unsafe precondition(s) violated when C passes NULL. Release builds skip
those checks, and the same call can appear to work. Don’t depend on this debug
check.
Slices to C
Handing a slice to C is simpler. as_ptr and len give you the two halves, and
as_mut_ptr gives you a *mut T for C functions that write into the buffer:
void scale_all(int32_t *items, size_t len, int32_t factor);
// Stands in for the C implementation, so that this example links.
#[unsafe(export_name = "scale_all")]
unsafe extern "C" fn scale_all_impl(items: *mut i32, len: usize, factor: i32) {
let items = unsafe { std::slice::from_raw_parts_mut(items, len) };
items.iter_mut().for_each(|item| *item *= factor);
}
unsafe extern "C" {
fn scale_all(items: *mut i32, len: usize, factor: i32);
}
let mut items = [1_i32, 2, 3];
// SAFETY: the pointer and length describe `items`, which isn't accessed
// elsewhere during the call.
unsafe { scale_all(items.as_mut_ptr(), items.len(), 2) };
One thing to watch out for: The pointer of an empty slice is dangling: non-null,
but not pointing to any memory. C code that checks ptr != NULL instead of
len != 0 will cause problems.
This only lends the memory to C for the duration of the call. That’s also why
there’s no into_raw_parts to mirror from_raw_parts: a slice borrows its
memory, so it has no ownership to give away. Handing ownership over is a
different matter, which
Allocation, allocators, and mixing them
covers.
Head to the exercise
The exercise asks you to implement two functions that take a pointer and a length the way C would pass them: one that reads the items, and one that modifies them in place. The tests call them with normal input, and with both ways of saying “no items”: a null pointer with a length of 0, and a valid pointer with a length of 0.
Exercise
The exercise for this section is located in 02_basics/05_slices