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.