Что такое unsafe код в Rust и когда его стоит использовать?

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

Несмотря на устрашающее название, unsafe не делает код "небезопасным" по определению — это скорее знак: «Здесь Rust не может проверить безопасность, будь внимателен сам».

Зачем вообще нужен unsafe?

Rust создан как язык безопасного управления памятью без сборщика мусора. Его система владения, заимствования и заимствования с проверкой времени жизни (ownership, borrowing, lifetimes) обеспечивает гарантии отсутствия:

  • гонок данных;

  • use-after-free;

  • двойного освобождения памяти;

  • разыменования нулевых и висячих указателей.

Но эти гарантии достигаются за счёт ограничений. Иногда они слишком жёсткие, и тогда без unsafe невозможно:

  • работать с низкоуровневыми структурами (например, операционной системой или железом);

  • взаимодействовать с C-библиотеками;

  • выполнять системные вызовы;

  • управлять памятью вручную;

  • оптимизировать производительность в критичных участках.

Что разрешено в unsafe-блоке?

Внутри unsafe вы можете делать операции, запрещённые в безопасном (safe) Rust. Конкретно:

Разыменовывать указатели (*const T, *mut T)
Эти "сырые указатели" не проверяются компилятором. Чтобы получить доступ к их содержимому, нужно использовать unsafe.

<br/>let ptr: \*const i32 = &10;
unsafe {
println!("{}", \*ptr); // разыменование указателя
}

1. Вызывать unsafe функции или методы. Некоторые функции, даже из стандартной библиотеки, помечены как unsafe, потому что требуют от вызывающего соблюдения определённых условий.

<br/>unsafe fn dangerous() {}
unsafe {
dangerous();
}

2. Доступ к union-типам. Rust позволяет использовать union (объединения), как в C, но работа с ними требует unsafe, так как они не гарантируют корректную интерпретацию памяти.

3. Вызывать внешние (FPI) функции. Все функции, объявленные через extern "C", потенциально небезопасны, поскольку Rust не знает, как они устроены.

<br/>extern "C" {
fn abs(input: i32) -> i32;
}
unsafe {
println!("{}", abs(-3));
}

4. Изменять переменные через static mut. Изменение глобальных переменных небезопасно, особенно если возможен одновременный доступ из разных потоков.

<br/>static mut COUNTER: i32 = 0;
unsafe {
COUNTER += 1;
}

5. Реализовать небезопасные трейты (unsafe trait). Некоторые трейты связаны с низкоуровневой логикой (например, Send, Sync) и требуют особой осторожности при реализации.

Когда стоит использовать unsafe?

Использование unsafe оправдано, если:

  • Нужно взаимодействие с внешним миром
    Например, с системными API, драйверами, устройствами, нативными библиотеками на C/C++.

  • Нужна максимальная производительность
    В системах реального времени, играх, ядрах ОС — допустимо жертвовать абстрактной безопасностью ради контроля.

  • Требуется прямой контроль над памятью
    Например, при реализации собственных аллокаторов, арен, пулов объектов или специализированных контейнеров.

  • Вы реализуете низкоуровневые абстракции
    Например, стандартные структуры, как Vec<T>, Box<T> или Rc<T> — внутри них часто используется unsafe, но они предоставляют безопасный API наружу.

  • Вы создаете FFI-интерфейсы
    То есть связываете Rust-код с библиотеками на других языках — тут безопасность невозможно гарантировать автоматически.

Как писать безопасно даже в unsafe?

Rust не запрещает unsafe, но требует, чтобы программист локализовал опасный код:

  • Оборачивайте unsafe в безопасные абстракции. В идеале, опасность должна быть спрятана "внутри", а внешний API — безопасным.

  • Пишите комментарии к unsafe-блокам, объясняя, почему использование допустимо и какие инварианты должны соблюдаться.

  • Используйте минимально необходимую область unsafe. Не делайте весь модуль или функцию unsafe, если нужна всего одна строчка.

  • Пользуйтесь инструментами: miri, cargo-geiger, sanitizers, чтобы обнаружить проблемы в unsafe-коде.

  • Старайтесь избегать unsafe, если есть способ решить задачу через safe-абстракции. Rust часто предлагает такие альтернативы.

Примеры, где unsafe незаменим

  • Реализация контейнеров: Vec, LinkedList, Box.

  • Низкоуровневая сериализация/десериализация бинарных структур.

  • Выравнивание данных и SIMD-инструкции.

  • Работа с shared-memory или memory-mapped файлами.

  • Создание пользовательских аллокаторов (GlobalAlloc).

  • Установка сигналов, хуков, взаимодействие с регистровыми структурами процессора.

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