сеньорчикОткрыть в Telegram
← вся теориятеория к собесу

unsafe в Rust

Что такое unsafe

Про 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, остальные разбираются в тренажёре.

  1. #rs_unsafe1 / 5
    Что такое безопасная абстракция над unsafe?
    A)Код, где unsafe спрятан за макросом и не виден в исходниках
    B)Функция, помеченная unsafe, но не нарушающая правил языка
    C)Обёртка, которая ловит нарушения инвариантов в рантайме и паникует
    D)Публичное API без unsafe: инварианты держит автор
    показать ответ и разбор
    +D)Публичное API без unsafe: инварианты держит автор

    // разбор: Так устроена половина стандартной библиотеки: Vec, String и split_at_mut внутри работают с сырыми указателями, а снаружи безопасны, потому что автор доказал инварианты — длина не больше ёмкости, срезы не пересекаются. Пользователю unsafe писать не нужно, а цена доверия сосредоточена в одном небольшом модуле.

  2. #rs_unsafe2 / 5
    Чем unsafe fn отличается от unsafe-блока внутри обычной функции?
    A)unsafe fn перекладывает проверку условий на вызывающего
    B)unsafe-блок допускает больше операций, чем тело unsafe fn
    C)unsafe fn выполняется быстрее: компилятор снимает проверки границ
    D)Разницы нет: обе формы помечают код как непроверяемый
    показать ответ и разбор
    +A)unsafe fn перекладывает проверку условий на вызывающего

    // разбор: Пометка функции — это контракт: «вызывать можно лишь при таких-то условиях, проверить их обязан вызывающий». Именно поэтому у unsafe fn в документации есть раздел Safety. Блок внутри обычной функции говорит обратное: автор проверил всё сам, снаружи API безопасно. Проверки границ в рантайме ни та, ни другая форма не снимает.

  3. #rs_unsafe3 / 5
    Что считается неопределённым поведением (UB) в Rust?
    A)Ситуация, когда программа даёт разный результат на разных платформах
    B)Падение программы, включая панику и выход за границу массива
    C)Слом правил, на которые опирается компилятор: висячие ссылки, гонки
    D)Обращение к памяти, освобождённой сборщиком мусора
    показать ответ и разбор
    +C)Слом правил, на которые опирается компилятор: висячие ссылки, гонки

    // разбор: UB — это не «упало», а «компилятор имел право предполагать, что такого не бывает». Ссылка на освобождённую память, два &mut на одно значение, bool с байтом 2, гонка данных — после такого оптимизатор вправе выкинуть код или переставить его. Паника и выход за границу массива к UB не относятся: это определённое поведение с диагностикой.

  4. #rs_unsafe4 / 5
    Чем проверяют unsafe-код на скрытое UB?
    A)clippy с включённым набором правил pedantic
    B)Miri — интерпретатор MIR, ловит нарушения модели памяти
    C)Отладчик gdb с точками останова на обращениях к памяти
    D)Компилятор в режиме -O: оптимизатор сообщает о найденных нарушениях
    показать ответ и разбор
    +B)Miri — интерпретатор MIR, ловит нарушения модели памяти

    // разбор: Miri выполняет код по правилам абстрактной машины Rust и ловит то, что не видно в обычной сборке: чтение неинициализированной памяти, выход за пределы выделения, нарушение правил алиасинга. Работает медленно и не покрывает FFI, поэтому его гоняют на юнит-тестах вокруг unsafe. Clippy разбирает стиль, компилятор о UB не предупреждает.

  5. #rs_unsafe5 / 5
    Отключает ли unsafe проверку заимствований?
    A)Да, внутри блока правила заимствования не действуют и компилятор их пропускает
    B)Да, но только для сырых указателей, объявленных внутри этого же блока кода
    C)Нет: проверки остаются, блок лишь разрешает пять дополнительных операций
    D)Нет, но внутри блока предупреждения borrow checker понижаются до уровня подсказок
    показать ответ и разбор
    +C)Нет: проверки остаются, блок лишь разрешает пять дополнительных операций

    // разбор: Внутри unsafe работают все обычные правила: владение, заимствование, лайфтаймы, проверка типов. Блок добавляет ровно пять возможностей — разыменовать сырой указатель, вызвать unsafe-функцию, обратиться к изменяемому static, прочитать поле union, реализовать unsafe-трейт. Отсюда и распространённое заблуждение «unsafe отключает проверки»: он снимает конкретные запреты, а не защиту целиком.

дальше

Теорию прочитали. Навык ставится повторением

В Сеньорчике эта подтема идёт в ежедневных сессиях: движок возвращает её, пока ответы не станут уверенными, и ведёт прогресс отдельно по каждой подтеме. Теория внутри тоже бесплатна, лимит только на количество вопросов в день.