Создание безопасной абстракции над небезопасным кодом
Просто потому, что функция содержит небезопасный код, это не означает, что мы должны пометить всю функцию как небезопасную. На самом деле оборачивание небезопасного кода в безопасную функцию - это распространенная абстракция. В качестве примера давайте изучим функцию split_at_mut
из стандартной библиотеки, которая требует некоторого небезопасного кода. Мы исследуем, как ее можно реализовать. Этот безопасный метод определяется для изменяемых срезов: он берет один срез и делит его на два, деля срез по индексу, заданному в качестве аргумента. Листинг 19-4 показывает, как использовать 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]);
Листинг 19-4: Использование безопасной функции split_at_mut
Мы не можем реализовать эту функцию только с использованием safe Rust. Попытка может выглядеть примерно как в Листинге 19-5, который не скомпилируется. Для простоты мы реализуем split_at_mut
в виде функции, а не метода, и только для срезов значений i32
, а не для обобщенного типа 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..])
}
Листинг 19-5: Пытаемая реализация split_at_mut
только с использованием safe Rust
Эта функция сначала получает общую длину среза. Затем она проверяет, что индекс, заданный в качестве параметра, находится внутри среза, сравнивая его с длиной. Это утверждение означает, что если мы передадим индекс, превышающий длину, чтобы разделить срез, функция будет завершаться с паникой, прежде чем попытается использовать этот индекс.
Затем мы возвращаем два изменяемых среза в кортеже: один от начала исходного среза до индекса mid
и другой от mid
до конца среза.
Когда мы пытаемся скомпилировать код из Листинга 19-5, мы получим ошибку:
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 не может понять, что мы берем разные части среза; он только знает, что мы берем из одного и того же среза дважды. Взятие разных частей среза по существу допустимо, потому что два среза не перекрываются, но Rust недостаточно умный, чтобы это знать. Когда мы знаем, что код корректен, но Rust не знает, настало время использовать небезопасный код.
Листинг 19-6 показывает, как использовать небезопасный блок, сырой указатель и несколько вызовов небезопасных функций, чтобы сделать реализацию split_at_mut
работоспособной.
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),
)
}
}
Листинг 19-6: Использование небезопасного кода в реализации функции split_at_mut
Помните из раздела "Тип среза", что срез - это указатель на некоторые данные и длина среза. Мы используем метод len
, чтобы получить длину среза [1], и метод as_mut_ptr
, чтобы получить сырой указатель среза [2]. В этом случае, поскольку у нас есть изменяемый срез значений i32
, as_mut_ptr
возвращает сырой указатель с типом *mut i32
, который мы сохранили в переменной ptr
.
Мы сохраняем утверждение, что индекс mid
находится внутри среза [3]. Затем мы доходим до небезопасного кода [4]: функция slice::from_raw_parts_mut
принимает сырой указатель и длину и создает срез. Мы используем ее, чтобы создать срез, который начинается с ptr
и имеет длину mid
элементов [5]. Затем мы вызываем метод add
на ptr
с аргументом mid
, чтобы получить сырой указатель, который начинается с mid
, и создаем срез с использованием этого указателя и оставшимся количеством элементов после mid
в качестве длины [6].
Функция slice::from_raw_parts_mut
небезопасна, потому что она принимает сырой указатель и должна доверять, что этот указатель действителен. Метод add
для сырых указателей также небезопасен, потому что он должен доверять, что смещение также является действительным указателем. Поэтому мы должны поместить небезопасный блок вокруг вызовов slice::from_raw_parts_mut
и add
, чтобы мы могли их вызвать. Смотрев на код и добавив утверждение, что mid
должен быть меньше или равен len
, мы можем сказать, что все сырые указатели, используемые внутри небезопасного блока, будут действительными указателями на данные внутри среза. Это приемлемое и подходящее использование unsafe
.
Обратите внимание, что мы не должны помечать результирующую функцию split_at_mut
как unsafe
, и мы можем вызвать эту функцию из safe Rust. Мы создали безопасную абстракцию для небезопасного кода с реализацией функции, которая использует небезопасный код безопасным образом, потому что она создает только действительные указатели из данных, к которым имеет доступ эта функция.
В отличие от этого, использование slice::from_raw_parts_mut
в Листинге 19-7, вероятно, вызовет аварийное завершение при использовании среза. Этот код берет произвольное место в памяти и создает срез длиной в 10 000 элементов.
use std::slice;
let address = 0x01234usize;
let r = address as *mut i32;
let values: &[i32] = unsafe {
slice::from_raw_parts_mut(r, 10000)
};
Листинг 19-7: Создание среза из произвольного места в памяти
Мы не владеем памятью в этом произвольном месте, и не гарантируется, что срез, созданный этим кодом, содержит действительные значения i32
. Попытка использовать values
как действительный срез приводит к неопределенному поведению.