Что означает “unsafe” в Rust?

Опубликовано 7 сентября 2025 г.

ПрограммированиеRust-Lang

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

Дыры в безопасности памяти Rust стремится к тому, чтобы код по умолчанию был безопасен с точки зрения памяти: не мог аварийно завершиться или быть скомпрометирован из-за висячих указателей или невалидных итераторов. Но есть вещи, которые невозможно выразить через систему типов. Например, нельзя сделать полностью безопасными низкоуровневые взаимодействия с ОС и системными библиотеками (например, аллокаторами памяти или потоками). Человеческое знание о том, как использовать их безопасно, должно быть где-то зафиксировано, но компилятор не может это проверить: ошибки возможны.

В других безопасных языках (например, Python или Haskell) такие знания зашиты в реализации виртуальных машин/рантаймов, обычно написанных на C. В Rust нет тяжёлого VM или рантайма, но всё равно нужно предоставлять (желательно безопасные) интерфейсы.

Rust решает это с помощью ключевого слова unsafe, которое позволяет использовать потенциально опасные возможности: вызовы ОС и внешних библиотек через FFI, работу с “сырыми” указателями и т.д.

Вся стандартная библиотека Rust построена с использованием unsafe: большая её часть написана на Rust, включая такие фундаментальные типы, как Rc, Vec, HashMap, с минимальным количеством C-кода и внешних библиотек (jemalloc, libuv).

unsafe Есть два способа воспользоваться опасными возможностями: через блок unsafe или функцию, помеченную как unsafe.

// вызов C-функций через FFI:

unsafe fn foo() {
    some_c_function();
}
fn bar() {
    unsafe {
        another_c_function();
    }
}
fn baz() {
    // ошибка: не в unsafe-контексте
    // yet_another_c_function();
}

В unsafe-контексте разрешено (неполный список):

Все эти действия могут привести к серьёзным проблемам. Например, ссылка &T — это машинный указатель, который всегда должен указывать на валидное значение типа T; все четыре пункта выше могут это нарушить:

Есть функция std::mem::transmute, которая интерпретирует байты аргумента как любой другой тип; так можно создать невалидную ссылку, например: transmute::<uint, &Vec<int>>(0).

Сырой указатель p: *const T может быть равен NULL. Операция &*p создаёт ссылку &T, указывающую на данные по адресу p. Если p — это NULL, получится невалидная ссылка.

Если есть static mut X: Option<i64> = Some(1234);, можно получить ссылку r: &i64 на 1234, но другой поток может заменить X на None, и тогда r станет висячей.

Inline-ассемблер может установить любые значения в любые регистры, включая установку нуля в регистр, где ожидается &T.

Что на самом деле означает unsafe? Unsafe-контекст — это обещание программиста компилятору, что код безопасен благодаря инвариантам, которые невозможно выразить в системе типов, и что соблюдаются все инварианты, которые требует сам Rust.

Эти инварианты должны соблюдаться даже внутри unsafe-блоков, и компилятор компилирует и оптимизирует код, исходя из этого. Нарушение этих инвариантов — это неопределённое поведение¹, и программа может делать “что угодно”.

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

Безопасная функция, использующая внутри unsafe, должна быть безопасна для вызова: не должно быть ни одного набора аргументов, при которых она нарушит инварианты. Если такие случаи есть, функцию нужно пометить как unsafe.

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

Пример: Vec Тип Vec<T> определён так:

pub struct Vec<T> {
    len: uint,
    cap: uint,
    ptr: *mut T
}

Здесь есть как минимум два инварианта:

Выразить это в системе типов Rust невозможно, поэтому реализация должна гарантировать эти инварианты вручную, используя unsafe. Компилятор не может проверить их, поэтому требует явного opt-in:

fn as_slice<'a>(&'a self) -> &'a [T] {
    unsafe { mem::transmute(Slice { data: self.as_ptr(), len: self.len }) }
}

Если бы здесь случайно использовали self.cap вместо self.len, срез был бы слишком длинным, и часть данных оказалась бы неинициализированной. Компилятор не может это проверить, поэтому требует unsafe.

Важно, что эти инварианты должны соблюдаться всегда, иначе Vec позволит нарушить безопасность через свои безопасные методы (например, если кто-то увеличит len без инициализации элементов, as_slice будет работать некорректно).

Компилятор не может это гарантировать, поэтому API Vec делает поля приватными и аккуратно помечает опасные методы как unsafe, например, set_len.

Пример: malloc C-функция malloc описывается так:

Функция malloc() выделяет size байт и возвращает указатель на выделенную память. Память не инициализирована. Если size == 0, malloc() возвращает либо NULL, либо уникальный указатель, который можно передать в free().

malloc() и calloc() возвращают указатель, выровненный для любого встроенного типа. При ошибке возвращается NULL.

Crate libc определяет большинство символов из libc, включая libc::malloc. Напишем безопасную программу, которая выделяет память под i64, сохраняет и выводит его, подробно объясняя, почему каждый unsafe здесь безопасен (в идеале каждый unsafe-блок должен быть обоснован).

extern crate libc;
use std::ptr;

fn main() {
    let pointer: *mut i64 = unsafe {
        // rustc не знает, что делает malloc, и не может гарантировать,
        // что вызов с аргументом 8 всегда безопасен; но мы знаем,
        // поэтому используем unsafe. (malloc возвращает *mut libc::c_void,
        // поэтому нужно привести к нужному типу.)
        libc::malloc(8) as *mut i64
    };

    // мы знаем, что единственная ошибка — это NULL, иначе указатель валиден
    if pointer.is_null() {
        println!("could not allocate");
    } else {
        // память валидна, но не инициализирована, поэтому инициализируем её.
        // Используем std::ptr::write, чтобы не вызвать деструкторы для старых данных.
        unsafe {
            ptr::write(pointer, 1234i64);
        }

        // теперь pointer указывает на инициализированную память,
        // можно читать и получать ссылку.
        let data: &i64 = unsafe { &*pointer };
        println!("The data is {}", *data);
        // вывод: The data is 1234
    }

    // (утечка памяти не считается unsafe)
}

(Заметим, что у i64 нет деструктора, поэтому ptr::write здесь не обязателен, но это хорошая практика.)

FAQ: Почему unsafe не “заражает” всё? Можно ожидать, что функция с unsafe-блоком должна быть unsafe для вызова, как в Haskell все нечистые вычисления помечаются IO.

Но это не так: unsafe — это лишь деталь реализации; если безопасная функция использует внутри unsafe, это значит, что автору пришлось обойти систему типов, но интерфейс остаётся безопасным.

Если бы unsafe был “заразным”, вся стандартная библиотека Rust (и все программы на Rust) были бы полностью unsafe, ведь она построена на unsafe-внутренностях.

Вывод Маркер unsafe — это способ обойти систему типов Rust, сообщая компилятору, что есть внешние условия/инварианты, гарантирующие корректность: компилятор отступает и доверяет программисту. Это позволяет писать низкоуровневый код, как на C, но по умолчанию оставаться безопасным, заставляя явно отмечать рискованные места.