Unsafe Rust
| Язык | Rust |
|---|---|
| Ключевое слово | unsafe (блоки, функции, трейты, impl, extern-блоки) |
| Пять «суперспособностей» | сырые указатели, unsafe-функции, изменяемые static, unsafe-трейты, поля union |
| Что не отключается | проверка заимствований, типов, времён жизни для обычных ссылок |
| Инструменты проверки | Miri, санитайзеры, cargo-geiger |
| Справочник | The Rustonomicon |
Unsafe Rust — часть языка Rust, в которой программист обещает компилятору соблюдать инварианты, которые тот не может проверить сам. Обычный («безопасный») Rust гарантирует отсутствие висячих указателей, гонок данных и выхода за границы через систему владения и заимствования. Но некоторые вещи — работа с железом, вызов библиотек на C, реализация эффективных структур данных — принципиально требуют операций, корректность которых компилятор доказать не может. Для них есть ключевое слово unsafe.[1]
Что разрешает unsafe
unsafe не выключает проверки Rust: заимствования, типы и времена жизни обычных ссылок проверяются как всегда. Он лишь открывает пять дополнительных возможностей:[1]
- разыменовывать сырые указатели
*const Tи*mut T; - вызывать unsafe-функции, включая функции из других языков (FFI);
- читать и изменять изменяемые статические переменные (
static mut); - реализовывать unsafe-трейты (например,
SendиSyncвручную); - обращаться к полям
union.
let mut x = 10;
let p = &mut x as *mut i32; // создать сырой указатель можно и в safe-коде
unsafe {
*p += 1; // а разыменовать — только в unsafe
}
assert_eq!(x, 11);
Неопределённое поведение
Если внутри unsafe нарушить правила языка, возникает неопределённое поведение (UB): компилятор вправе оптимизировать код, исходя из того, что нарушения не бывает, и программа может вести себя как угодно — не только «упасть». К UB относятся:[3]
- разыменование висячего, нулевого или невыровненного указателя;
- гонка данных;
- одновременное существование
&mut Tи другой ссылки на те же данные; - создание недопустимого значения:
bool, отличного от 0 и 1, неинициализированной памяти как целого числа, невалидного UTF-8 вstr; - вызов функции с неверным ABI.
Важная мысль Rustonomicon: ошибка может быть в безопасном коде рядом, если он нарушает инвариант, на который опирается unsafe-блок.[2] Поэтому границей «опасной зоны» на практике считается модуль, а не блок.
Безопасные абстракции
Главный приём — инкапсулировать unsafe внутри функции или типа с безопасным интерфейсом, который невозможно использовать неправильно. Вся стандартная библиотека устроена так: Vec, String, HashMap, Rc, Mutex внутри используют сырые указатели и ручное управление памятью, но снаружи безопасны.
pub fn split_at_mut<T>(s: &mut [T], mid: usize) -> (&mut [T], &mut [T]) {
let len = s.len();
let ptr = s.as_mut_ptr();
assert!(mid <= len); // проверка делает unsafe-код корректным
// SAFETY: части не пересекаются, обе лежат внутри исходного среза.
unsafe {
(
std::slice::from_raw_parts_mut(ptr, mid),
std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
Принятые соглашения:[7]
- каждый
unsafe-блок сопровождается комментарием// SAFETY:с объяснением, почему он корректен; - у каждой
unsafe fnв документации есть раздел# Safetyс условиями, которые должен выполнить вызывающий; - линт Clippy
undocumented_unsafe_blocksследит за этим (см. Clippy и rustfmt).
В редакции Rust 2024 тело unsafe fn больше не считается одним большим unsafe-блоком: опасные операции внутри неё тоже нужно оборачивать в unsafe { }, а блоки extern пишутся как unsafe extern "C".[5]
FFI
Вызов функций из библиотек на C — самая частая причина использовать unsafe:
unsafe extern "C" {
fn strlen(s: *const std::ffi::c_char) -> usize;
}
let s = std::ffi::CString::new("привет").unwrap();
let n = unsafe { strlen(s.as_ptr()) }; // 12 байт в UTF-8
Компилятор не знает, что делает чужой код, поэтому ответственность за корректные аргументы лежит на программисте. Обёртки генерируют инструменты bindgen (из заголовков C) и cxx (для C++), а для экспорта Rust в C — cbindgen. Через тот же механизм Rust встраивается в Python (PyO3) и компилируется в WebAssembly.
Сколько unsafe в реальном коде
Исследование 2020 года по крейтам crates.io показало, что около 76 % крейтов не содержат unsafe вовсе, а в остальных он сосредоточен в небольших участках — чаще всего для FFI и оптимизаций.[6] Посчитать unsafe в зависимостях проекта помогает cargo-geiger.
Как проверять
- Miri — интерпретатор промежуточного представления Rust, который во время выполнения тестов обнаруживает многие виды UB: выход за границы, использование после освобождения, нарушения правил алиасинга:[4]
rustup +nightly component add miri
cargo +nightly miri test
- Санитайзеры (AddressSanitizer, ThreadSanitizer) через флаги nightly-компилятора.
- Фаззинг (
cargo fuzz) для кода, разбирающего внешние данные. - Ревью:
unsafe-код проверяют особенно тщательно, многие проекты запрещают его атрибутом#![forbid(unsafe_code)].
Unsafe и безопасность памяти
Наличие unsafe не отменяет преимуществ Rust перед C и C++: потенциально опасный код явно помечен, локализован и поддаётся аудиту, а остальная программа проверяется компилятором. Это противоположность языкам с ручным управлением памятью, где «опасна» любая строка (см. Сборка мусора vs ручное управление памятью).
См. также
Источники
- The Rust Programming Language — Unsafe Rust
- The Rustonomicon
- The Rust Reference — Behavior considered undefined
- Miri — an interpreter for Rust's mid-level IR
- Rust 2024 Edition Guide — Unsafe extern blocks and unsafe_op_in_unsafe_fn
- Astrauskas et al. — How do programmers use unsafe Rust? (OOPSLA 2020)
- Rust API Guidelines — Documentation (Safety sections)