Creating a Safe Abstraction over Unsafe Code
Just because a function contains unsafe code doesn't mean we need to mark the entire function as unsafe. In fact, wrapping unsafe code in a safe function is a common abstraction. As an example, let's study the split_at_mut function from the standard library, which requires some unsafe code. We'll explore how we might implement it. This safe method is defined on mutable slices: it takes one slice and makes it two by splitting the slice at the index given as an argument. Listing 19-4 shows how to use split_at_mut.
let mut v = vec![1, 2, 3, 4, 5, 6];
let r = &mut v[..];
let (a, b) = r.split_at_mut(3);
assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);
Listing 19-4: Using the safe split_at_mut function
We can't implement this function using only safe Rust. An attempt might look something like Listing 19-5, which won't compile. For simplicity, we'll implement split_at_mut as a function rather than a method and only for slices of i32 values rather than for a generic type T.
fn split_at_mut(
values: &mut [i32],
mid: usize,
) -> (&mut [i32], &mut [i32]) {
let len = values.len();
assert!(mid <= len);
(&mut values[..mid], &mut values[mid..])
}
Listing 19-5: An attempted implementation of split_at_mut using only safe Rust
This function first gets the total length of the slice. Then it asserts that the index given as a parameter is within the slice by checking whether it's less than or equal to the length. The assertion means that if we pass an index that is greater than the length to split the slice at, the function will panic before it attempts to use that index.
Then we return two mutable slices in a tuple: one from the start of the original slice to the mid index and another from mid to the end of the slice.
When we try to compile the code in Listing 19-5, we'll get an error:
error[E0499]: cannot borrow `*values` as mutable more than once at a time
--> src/main.rs:9:31
|
2 | values: &mut [i32],
| - let's call the lifetime of this reference `'1`
...
9 | (&mut values[..mid], &mut values[mid..])
| --------------------------^^^^^^--------
| | | |
| | | second mutable borrow occurs here
| | first mutable borrow occurs here
| returning this value requires that `*values` is borrowed for `'1`
Rust's borrow checker can't understand that we're borrowing different parts of the slice; it only knows that we're borrowing from the same slice twice. Borrowing different parts of a slice is fundamentally okay because the two slices aren't overlapping, but Rust isn't smart enough to know this. When we know code is okay, but Rust doesn't, it's time to reach for unsafe code.
Listing 19-6 shows how to use an unsafe block, a raw pointer, and some calls to unsafe functions to make the implementation of split_at_mut work.
use std::slice;
fn split_at_mut(
values: &mut [i32],
mid: usize,
) -> (&mut [i32], &mut [i32]) {
1 let len = values.len();
2 let ptr = values.as_mut_ptr();
3 assert!(mid <= len);
4 unsafe {
(
5 slice::from_raw_parts_mut(ptr, mid),
6 slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
Listing 19-6: Using unsafe code in the implementation of the split_at_mut function
Recall from "The Slice Type" that a slice is a pointer to some data and the length of the slice. We use the len method to get the length of a slice [1] and the as_mut_ptr method to access the raw pointer of a slice [2]. In this case, because we have a mutable slice to i32 values, as_mut_ptr returns a raw pointer with the type *mut i32, which we've stored in the variable ptr.
We keep the assertion that the mid index is within the slice [3]. Then we get to the unsafe code [4]: the slice::from_raw_parts_mut function takes a raw pointer and a length, and it creates a slice. We use it to create a slice that starts from ptr and is mid items long [5]. Then we call the add method on ptr with mid as an argument to get a raw pointer that starts at mid, and we create a slice using that pointer and the remaining number of items after mid as the length [6].
The function slice::from_raw_parts_mut is unsafe because it takes a raw pointer and must trust that this pointer is valid. The add method on raw pointers is also unsafe because it must trust that the offset location is also a valid pointer. Therefore, we had to put an unsafe block around our calls to slice::from_raw_parts_mut and add so we could call them. By looking at the code and by adding the assertion that mid must be less than or equal to len, we can tell that all the raw pointers used within the unsafe block will be valid pointers to data within the slice. This is an acceptable and appropriate use of unsafe.
Note that we don't need to mark the resultant split_at_mut function as unsafe, and we can call this function from safe Rust. We've created a safe abstraction to the unsafe code with an implementation of the function that uses unsafe code in a safe way, because it creates only valid pointers from the data this function has access to.
In contrast, the use of slice::from_raw_parts_mut in Listing 19-7 would likely crash when the slice is used. This code takes an arbitrary memory location and creates a slice 10,000 items long.
use std::slice;
let address = 0x01234usize;
let r = address as *mut i32;
let values: &[i32] = unsafe {
slice::from_raw_parts_mut(r, 10000)
};
Listing 19-7: Creating a slice from an arbitrary memory location
We don't own the memory at this arbitrary location, and there is no guarantee that the slice this code creates contains valid i32 values. Attempting to use values as though it's a valid slice results in undefined behavior.