Перейти к содержимому

Unsafe в Rust

Каждую гарантию, о которой шла речь в этом курсе, — никаких висячих ссылок, никаких гонок данных, никакого выхода за границы — обеспечивает компилятор. unsafe — это способ сказать компилятору: «дай мне сделать то, безопасность чего ты не можешь проверить; ответственность я беру на себя».

Частое заблуждение — будто unsafe полностью отключает проверки безопасности Rust. Это не так. Внутри блока unsafe можно делать ровно пять дополнительных вещей, которые в остальном коде запрещены:

  1. Разыменовать сырой указатель (raw pointer)
  2. Вызвать функцию, помеченную unsafe
  3. Читать или изменять изменяемую static-переменную
  4. Реализовать unsafe-трейт
  5. Обращаться к полям union

Всё остальное — правила заимствования, семантика перемещения, проверка типов — внутри unsafe действует в полную силу. Это скальпель для пяти конкретных операций, а не выключатель.

Обычные ссылки Rust (& и &mut) всегда указывают на корректные, правильно выровненные данные — это часть того, что гарантирует компилятор. Сырой указатель (*const T или *mut T) этой гарантии не даёт, поэтому чтение через него требует unsafe:

fn main() {
let value = 5;
let pointer = &value as *const i32;
unsafe {
println!("{}", *pointer);
}
}

Вывод:

5
▶ Попробуй сам

Создать сырой указатель можно и в безопасном коде. unsafe нужен только для разыменования (чтения значения, на которое он указывает), потому что компилятор больше не может проверить, что указатель корректен.

Некоторые функции сами помечены unsafe, потому что у них есть требования, которые компилятор не может проверить за тебя. Вызывая такую функцию, ты обещаешь, что проверил эти требования сам:

unsafe fn dangerous() {
println!("эта функция верит, что ты проверил её требования");
}
fn main() {
unsafe {
dangerous();
}
}

Вывод:

эта функция верит, что ты проверил её требования
▶ Попробуй сам

В стандартной библиотеке есть настоящие unsafe-функции, которые можно вызывать. Одна из них — get_unchecked у срезов. Она работает как numbers[i], но пропускает проверку границ. Если i выходит за границы, она не паникует. Она читает ту память, которая там оказалась, и это называется неопределённым поведением (undefined behavior): программа может упасть, напечатать мусор или казаться нормальной — до поры до времени.

Поэтому правило для её вызова такое: «только с индексом, про который ты знаешь, что он корректен». Обычный приём — проверить это правило самому внутри обычной, безопасной функции:

fn first_or_zero(numbers: &[i32]) -> i32 {
if numbers.is_empty() {
return 0;
}
// SAFETY: срез не пустой, поэтому индекс 0 в пределах границ.
unsafe { *numbers.get_unchecked(0) }
}
fn main() {
println!("{}", first_or_zero(&[7, 8, 9]));
println!("{}", first_or_zero(&[]));
}

Вывод:

7
0
▶ Попробуй сам

Посмотри, что видит вызывающий код: fn first_or_zero(numbers: &[i32]) -> i32. В сигнатуре нет unsafe, и вызвать функцию неправильно невозможно, потому что она проверяет правило ещё до того, как дойдёт до блока unsafe. Сам unsafe остаётся маленьким и спрятанным внутри.

Две привычки, которых придерживаются опытные Rust-разработчики:

  • Пиши комментарий // SAFETY: над каждым блоком unsafe и объясняй, почему правила соблюдены. Это для следующего человека, который будет читать код, — и для тебя в будущем.
  • Делай блок unsafe как можно меньше, чтобы перепроверять пришлось меньше кода.

Если честно, именно такую функцию в реальном коде ты писать не станешь: numbers.first() делает то же самое безопасно, а компилятор очень хорошо убирает проверки границ, когда может доказать, что они не нужны. Но сам приём (проверить правила, потом вызвать unsafe внутри безопасной функции) — ровно то, как устроены Vec, String и их друзья. Страница FFI использует тот же трюк, чтобы обернуть функции на C.

Если Rust так заботится о безопасности, зачем вообще это разрешать? Потому что некоторые вещи действительно невозможно доказать безопасными во время компиляции, хотя на практике они совершенно безопасны: прямое общение с железом, вызов библиотеки на C (см. FFI) или создание некоторых высокопроизводительных структур данных. unsafe позволяет стандартной библиотеке (и изредка твоему собственному коду) реализовать такие низкоуровневые части, а всё, что построено поверх них, остаётся обычным, полностью проверяемым безопасным Rust.

Если ты написал unsafe, это не значит, что в коде теперь ошибки. Это значит, что в этом конкретном блоке компилятор больше не может поймать за тебя некоторые классы ошибок, и соблюдать те же гарантии приходится вручную. Большинство опытных Rust-разработчиков пишут совсем мало unsafe-кода напрямую. Обычно он живёт в небольших, тщательно проверенных уголках в самом низу библиотеки и обёрнут в безопасный API, которым пользуются все остальные.

Создай let mut score = 10;, сделай на него сырой указатель *mut i32 и через указатель прибавь 5. Потом выведи score. Подсказка: создай указатель через &mut score as *mut i32, а *pointer += 5 напиши внутри блока unsafe.

Показать решение
fn main() {
let mut score = 10;
let pointer = &mut score as *mut i32; // создавать его безопасно
unsafe {
*pointer += 5; // а писать через него — нет
}
println!("score = {score}");
}

Вывод:

score = 15
▶ Попробуй сам

На практике это безопасно, потому что score ещё жива и никто другой ею не пользуется, пока мы пишем. Как только ты используешь unsafe, соблюдать эти обещания — твоя работа.

Напиши fn get_or(numbers: &[i32], index: usize, default: i32) -> i32. Она возвращает элемент по индексу index или default, если индекс вне границ. Для чтения используй get_unchecked и сделай так, чтобы функцию никак нельзя было вызвать с нарушением её правила. Подсказка: сначала сравни index < numbers.len() и вызывай get_unchecked, только когда это верно.

Показать решение
fn get_or(numbers: &[i32], index: usize, default: i32) -> i32 {
if index < numbers.len() {
// SAFETY: мы только что проверили, что индекс в пределах границ.
unsafe { *numbers.get_unchecked(index) }
} else {
default
}
}
fn main() {
let numbers = [10, 20, 30];
println!("{}", get_or(&numbers, 1, -1));
println!("{}", get_or(&numbers, 5, -1));
}

Вывод:

20
-1
▶ Попробуй сам

Индекс 5 никогда не доходит до блока unsafe, поэтому обрабатывается безопасно. Для сравнения: полностью безопасная версия — numbers.get(index).copied().unwrap_or(default), и обычно ты написал бы именно её.

  • unsafe открывает ровно пять операций (в основном сырые указатели и вызов unsafe-функций) — всё остальное по-прежнему полностью проверяется.
  • Сырой указатель (*const T / *mut T) можно создать в безопасном коде; его разыменование требует unsafe.
  • Оборачивай unsafe в маленькую безопасную функцию, которая сначала проверяет правила, и объясняй в комментарии // SAFETY:, почему это корректно.
  • unsafe существует, чтобы можно было построить низкоуровневый код (доступ к железу, взаимодействие с C), а код, который им пользуется, оставался безопасным.
  • Новичку редко придётся писать его самому — сначала ищи безопасную абстракцию.

Проверь себя

1. Что на самом деле открывает блок unsafe?

2. Можно ли создать сырой указатель в безопасном коде?

3. Как часто новичку придётся писать unsafe самому?

4. Значит ли unsafe-код, что в программе теперь есть ошибки?

Результат: 0 / 4