unsafe в Rust
Про unsafe спрашивают, чтобы проверить, не считаешь ли ты его выключателем безопасности. Ответ «там отключаются проверки» сразу снижает грейд. Реальный вопрос - понимаешь ли ты, какие именно гарантии язык перекладывает на тебя.
Стержень: unsafe разрешает пять конкретных операций и не отменяет ни проверку типов, ни владение, ни заимствование.
// Формулировки: «что разрешает unsafe?», «чем unsafe fn отличается от блока?», «что такое UB?»
Закрытый список из пяти вещей
unsafe разрешает: разыменовать сырой указатель, вызвать unsafe-функцию или функцию из FFI (foreign function interface), обратиться к изменяемому static, прочитать поле union, реализовать unsafe-трейт. Всё. Проверка типов, правила владения и заимствования внутри блока работают ровно как снаружи.
Отсюда правило гигиены: блок делают коротким. Чем меньше кода внутри, тем точнее видно, за что именно автор взял ответственность, и тем легче это ревьюить. Здоровая практика - писать над каждым unsafe комментарий SAFETY с обоснованием.
// Классическая формулировка: unsafe не выключает проверки, он даёт доступ к операциям, корректность которых компилятор проверить не в состоянии.
- unsafe
- доступ к пяти непроверяемым операциям
- SAFETY-комментарий
- обоснование, почему конкретный unsafe корректен
Безопасная абстракция и unsafe fn
Половина стандартной библиотеки построена на unsafe: Vec, String, split_at_mut внутри работают с сырыми указателями. Но снаружи их API безопасно, потому что автор доказал инварианты - длина не превышает ёмкость, срезы не пересекаются. Так цена доверия сосредоточена в маленьком проверяемом модуле.
Отличие unsafe fn от unsafe-блока именно в том, кто отвечает. Помеченная функция говорит: «вызывать можно только при таких условиях, проверить их обязан вызывающий», поэтому у неё в документации есть раздел Safety. Блок внутри обычной функции говорит обратное: автор всё проверил, наружу выдаётся безопасный интерфейс.
// Ни та, ни другая форма не влияет на сгенерированный код: проверки границ в рантайме остаются на месте.
- safe abstraction
- безопасное API поверх внутреннего unsafe
- unsafe fn
- функция, условия вызова которой проверяет вызывающий
UB и как его ловят
Неопределённое поведение это не «программа упала». Это ситуация, о которой компилятор имел право предполагать, что её не бывает: висячая ссылка, две живые &mut на одно место, bool с байтом 2, гонка данных. Дальше оптимизатор пользуется этим предположением и вправе выбросить или переставить код.
Опаснее всего то, что симптомов может не быть годами, а сломается всё от смены версии компилятора или уровня оптимизации. Паника и выход за границу массива к UB (undefined behavior) не относятся это определённое поведение с внятной диагностикой.
// Ловят UB интерпретатором Miri: он выполняет код по правилам абстрактной машины Rust и замечает чтение неинициализированной памяти, выход за границы выделения, нарушение правил алиасинга. Работает медленно и не покрывает FFI, поэтому его гоняют на юнит-тестах вокруг unsafe.
- UB
- нарушение предположений, на которых строятся оптимизации
- Miri
- интерпретатор MIR (mid-level intermediate representation), ловящий нарушения модели памяти
Как отвечать: «Что даёт блок unsafe?»
Он даёт доступ к пяти операциям, корректность которых компилятор проверить не может: разыменование сырого указателя, вызов unsafe-функции или FFI, обращение к изменяемому static, чтение поля union и реализация unsafe-трейта. Всё остальное продолжает работать - типы, владение, заимствование внутри блока проверяются как обычно. То есть unsafe не выключает безопасность, а перекладывает конкретную проверку на меня. Поэтому блоки держу короткими и пишу над ними SAFETY-комментарий, а сам unsafe стараюсь прятать за безопасным API - как это сделано у Vec и split_at_mut.
Ты сразу опровергаешь миф про «отключение проверок», называешь конкретный список и добавляешь практику. Это ответ, после которого дальше спрашивают уже про UB, а не про основы.
На чём валятся
- −Говорят, что unsafe отключает borrow checker. Он продолжает работать.
- −Помечают unsafe целую функцию вместо короткого блока и размывают ответственность.
- −Путают UB с падением программы: UB может годами не давать симптомов.
- −Не знают про Miri и считают, что unsafe-код проверить нечем.
- −Не могут объяснить, почему Vec безопасен снаружи, хотя внутри полон unsafe.
Проверьте себя
Пять вопросов из банка по этой подтеме. Всего их 12, остальные разбираются в тренажёре.
- Что такое безопасная абстракция над unsafe?A)Код, где unsafe спрятан за макросом и не виден в исходникахB)Функция, помеченная unsafe, но не нарушающая правил языкаC)Обёртка, которая ловит нарушения инвариантов в рантайме и паникуетD)Публичное API без unsafe: инварианты держит автор
показать ответ и разбор
+D)Публичное API без unsafe: инварианты держит автор// разбор: Так устроена половина стандартной библиотеки: Vec, String и split_at_mut внутри работают с сырыми указателями, а снаружи безопасны, потому что автор доказал инварианты — длина не больше ёмкости, срезы не пересекаются. Пользователю unsafe писать не нужно, а цена доверия сосредоточена в одном небольшом модуле.
- Чем unsafe fn отличается от unsafe-блока внутри обычной функции?A)unsafe fn перекладывает проверку условий на вызывающегоB)unsafe-блок допускает больше операций, чем тело unsafe fnC)unsafe fn выполняется быстрее: компилятор снимает проверки границD)Разницы нет: обе формы помечают код как непроверяемый
показать ответ и разбор
+A)unsafe fn перекладывает проверку условий на вызывающего// разбор: Пометка функции — это контракт: «вызывать можно лишь при таких-то условиях, проверить их обязан вызывающий». Именно поэтому у unsafe fn в документации есть раздел Safety. Блок внутри обычной функции говорит обратное: автор проверил всё сам, снаружи API безопасно. Проверки границ в рантайме ни та, ни другая форма не снимает.
- Что считается неопределённым поведением (UB) в Rust?A)Ситуация, когда программа даёт разный результат на разных платформахB)Падение программы, включая панику и выход за границу массиваC)Слом правил, на которые опирается компилятор: висячие ссылки, гонкиD)Обращение к памяти, освобождённой сборщиком мусора
показать ответ и разбор
+C)Слом правил, на которые опирается компилятор: висячие ссылки, гонки// разбор: UB — это не «упало», а «компилятор имел право предполагать, что такого не бывает». Ссылка на освобождённую память, два &mut на одно значение, bool с байтом 2, гонка данных — после такого оптимизатор вправе выкинуть код или переставить его. Паника и выход за границу массива к UB не относятся: это определённое поведение с диагностикой.
- Чем проверяют unsafe-код на скрытое UB?A)clippy с включённым набором правил pedanticB)Miri — интерпретатор MIR, ловит нарушения модели памятиC)Отладчик gdb с точками останова на обращениях к памятиD)Компилятор в режиме -O: оптимизатор сообщает о найденных нарушениях
показать ответ и разбор
+B)Miri — интерпретатор MIR, ловит нарушения модели памяти// разбор: Miri выполняет код по правилам абстрактной машины Rust и ловит то, что не видно в обычной сборке: чтение неинициализированной памяти, выход за пределы выделения, нарушение правил алиасинга. Работает медленно и не покрывает FFI, поэтому его гоняют на юнит-тестах вокруг unsafe. Clippy разбирает стиль, компилятор о UB не предупреждает.
- Отключает ли unsafe проверку заимствований?A)Да, внутри блока правила заимствования не действуют и компилятор их пропускаетB)Да, но только для сырых указателей, объявленных внутри этого же блока кодаC)Нет: проверки остаются, блок лишь разрешает пять дополнительных операцийD)Нет, но внутри блока предупреждения borrow checker понижаются до уровня подсказок
показать ответ и разбор
+C)Нет: проверки остаются, блок лишь разрешает пять дополнительных операций// разбор: Внутри unsafe работают все обычные правила: владение, заимствование, лайфтаймы, проверка типов. Блок добавляет ровно пять возможностей — разыменовать сырой указатель, вызвать unsafe-функцию, обратиться к изменяемому static, прочитать поле union, реализовать unsafe-трейт. Отсюда и распространённое заблуждение «unsafe отключает проверки»: он снимает конкретные запреты, а не защиту целиком.
дальше
Теорию прочитали. Навык ставится повторением
В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.