Unsafe в Rust
Каждую гарантию, о которой шла речь в этом курсе, — никаких висячих ссылок, никаких гонок данных, никакого выхода за границы — обеспечивает компилятор. unsafe — это способ сказать компилятору: «дай мне сделать то, безопасность чего ты не можешь проверить; ответственность я беру на себя».
Что unsafe делает — и чего не делает
Заголовок раздела «Что unsafe делает — и чего не делает»Частое заблуждение — будто unsafe полностью отключает проверки безопасности Rust. Это не так. Внутри блока unsafe можно делать ровно пять дополнительных вещей, которые в остальном коде запрещены:
- Разыменовать сырой указатель (raw pointer)
- Вызвать функцию, помеченную
unsafe - Читать или изменять изменяемую
static-переменную - Реализовать
unsafe-трейт - Обращаться к полям
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-функции»Некоторые функции сами помечены unsafe, потому что у них есть требования, которые компилятор не может проверить за тебя. Вызывая такую функцию, ты обещаешь, что проверил эти требования сам:
unsafe fn dangerous() { println!("эта функция верит, что ты проверил её требования");}
fn main() { unsafe { dangerous(); }}Вывод:
эта функция верит, что ты проверил её требованияОборачиваем unsafe в безопасную функцию
Заголовок раздела «Оборачиваем unsafe в безопасную функцию»В стандартной библиотеке есть настоящие 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(&[]));}Вывод:
70Посмотри, что видит вызывающий код: fn first_or_zero(numbers: &[i32]) -> i32. В сигнатуре нет unsafe, и вызвать функцию неправильно невозможно, потому что она проверяет правило ещё до того, как дойдёт до блока unsafe. Сам unsafe остаётся маленьким и спрятанным внутри.
Две привычки, которых придерживаются опытные Rust-разработчики:
- Пиши комментарий
// SAFETY:над каждым блокомunsafeи объясняй, почему правила соблюдены. Это для следующего человека, который будет читать код, — и для тебя в будущем. - Делай блок
unsafeкак можно меньше, чтобы перепроверять пришлось меньше кода.
Если честно, именно такую функцию в реальном коде ты писать не станешь: numbers.first() делает то же самое безопасно, а компилятор очень хорошо убирает проверки границ, когда может доказать, что они не нужны. Но сам приём (проверить правила, потом вызвать unsafe внутри безопасной функции) — ровно то, как устроены Vec, String и их друзья. Страница FFI использует тот же трюк, чтобы обернуть функции на C.
Зачем вообще нужен unsafe
Заголовок раздела «Зачем вообще нужен unsafe»Если Rust так заботится о безопасности, зачем вообще это разрешать? Потому что некоторые вещи действительно невозможно доказать безопасными во время компиляции, хотя на практике они совершенно безопасны: прямое общение с железом, вызов библиотеки на C (см. FFI) или создание некоторых высокопроизводительных структур данных. unsafe позволяет стандартной библиотеке (и изредка твоему собственному коду) реализовать такие низкоуровневые части, а всё, что построено поверх них, остаётся обычным, полностью проверяемым безопасным Rust.
Честный компромисс
Заголовок раздела «Честный компромисс»Если ты написал unsafe, это не значит, что в коде теперь ошибки. Это значит, что в этом конкретном блоке компилятор больше не может поймать за тебя некоторые классы ошибок, и соблюдать те же гарантии приходится вручную. Большинство опытных Rust-разработчиков пишут совсем мало unsafe-кода напрямую. Обычно он живёт в небольших, тщательно проверенных уголках в самом низу библиотеки и обёрнут в безопасный API, которым пользуются все остальные.
Практика
Заголовок раздела «Практика»1. Меняем значение через сырой указатель
Заголовок раздела «1. Меняем значение через сырой указатель»Создай 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, соблюдать эти обещания — твоя работа.
2. Безопасная get_or
Заголовок раздела «2. Безопасная get_or»Напиши 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
Что почитать
Заголовок раздела «Что почитать»- Небезопасный код в Rust — книга «Язык программирования Rust» (перевод на русский): пять «суперспособностей unsafe» подробно, включая безопасный
split_at_mutповерх сырых указателей. - The Rustonomicon (на английском): официальное руководство о том, как правильно писать unsafe-код. Это продвинутый уровень, но первую главу стоит прочитать.
- Ключевое слово
unsafe— документация стандартной библиотеки (на английском): unsafe-блоки, функции и трейты, а также соглашение о комментариях// SAFETY:. - Unsafety — The Rust Reference (на английском): точный список операций, которым нужен
unsafe. slice::get_unchecked— документация стандартной библиотеки (на английском): настоящая unsafe-функция, в разделе «Safety» которой расписано правило, которое нужно соблюдать.