Небезопасный Rust

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

Небезопасный Rust существует потому, что по своей природе статический анализ консервативен. Когда компилятор пытается определить, соблюдает ли код гарантии, лучше отклонить некоторые допустимые программы, чем принять некоторые некорректные. Хотя код может быть в порядке, если у компилятора Rust недостаточно информации для уверенности, он отклонит код. В таких случаях вы можете использовать небезопасный код, чтобы сказать компилятору: «Доверься мне, я знаю, что делаю». Однако будьте осторожны: вы используете небезопасный Rust на свой страх и риск: если вы используете небезопасный код неправильно, могут возникнуть проблемы due to нарушения безопасности памяти, такие как разыменование нулевого указателя.

Другая причина существования небезопасной alter ego в Rust заключается в том, что базовое компьютерное оборудование по своей природе небезопасно. Если бы Rust не позволял вам выполнять небезопасные операции, вы не могли бы выполнять определённые задачи. Rust должен позволять вам заниматься низкоуровневым системным программированием, таким как прямое взаимодействие с операционной системой или даже написание собственной операционной системы. Работа с низкоуровневым системным программированием является одной из целей языка. Давайте explore, что мы можем делать с небезопасным Rust и как это делать.

Использование небезопасных сверхспособностей

Чтобы переключиться на небезопасный Rust, используйте ключевое слово unsafe и затем начните новый блок, содержащий небезопасный код. Вы можете выполнять пять действий в небезопасном Rust, которые нельзя делать в безопасном Rust, и мы называем их небезопасными сверхспособностями. Эти сверхспособности включают возможность:

  • Разыменовывать сырой указатель (raw pointer)
  • Вызывать небезопасную функцию или метод
  • Получать доступ или изменять изменяемую статическую переменную (mutable static variable)
  • Реализовывать небезопасный трейт (unsafe trait)
  • Получать доступ к полям объединений (unions)

Важно понимать, что unsafe не отключает проверку заимствований (borrow checker) и не отключает какие-либо другие проверки безопасности Rust: если вы используете ссылку в небезопасном коде, она всё равно будет проверяться. Ключевое слово unsafe только даёт вам доступ к этим пяти возможностям, которые затем не проверяются компилятором на безопасность памяти. Вы всё равно получите некоторую степень безопасности внутри небезопасного блока.

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

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

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

Давайте рассмотрим каждую из пяти небезопасных сверхспособностей по очереди. Мы также рассмотрим некоторые абстракции, которые предоставляют безопасный интерфейс для небезопасного кода.

Разыменование сырого указателя (Raw Pointer)

В главе 4, в разделе «Висячие ссылки», мы упоминали, что компилятор гарантирует, что ссылки всегда действительны. Небезопасный Rust имеет два новых типа, называемых сырыми указателями (raw pointers), которые похожи на ссылки. Как и ссылки, сырые указатели могут быть неизменяемыми или изменяемыми и записываются как *const T и *mut T соответственно. Звёздочка не является оператором разыменования; это часть имени типа. В контексте сырых указателей «неизменяемый» означает, что указатель не может быть напрямую присвоен после разыменования.

В отличие от ссылок и умных указателей, сырые указатели:

  • Могут игнорировать правила заимствования, имея одновременно неизменяемые и изменяемые указатели или несколько изменяемых указателей на одно и то же место
  • Не гарантируют, что указывают на допустимую память
  • Могут быть null
  • Не реализуют автоматическое очищение (automatic cleanup)

Отказываясь от принудительного соблюдения этих гарантий в Rust, вы можете пожертвовать гарантированной безопасностью в обмен на большую производительность или возможность взаимодействия с другим языком или оборудованием, где гарантии Rust не применимы.

В листинге 20-1 показано, как создать неизменяемый и изменяемый сырой указатель.

#![allow(unused)]
fn main() {
let mut num = 5;

let r1 = &raw const num;
let r2 = &raw mut num;
}

Листинг 20-1: Создание сырых указателей с помощью операторов сырого заимствования

Обратите внимание, что мы не включаем ключевое слово unsafe в этот код. Мы можем создавать сырые указатели в безопасном коде; мы просто не можем разыменовывать сырые указатели вне небезопасного блока, как вы увидите далее.

Мы создали сырые указатели, используя операторы сырого заимствования: &raw const num создаёт неизменяемый сырой указатель *const i32, а &raw mut num создаёт изменяемый сырой указатель *mut i32. Поскольку мы создали их непосредственно из локальной переменной, мы знаем, что эти конкретные сырые указатели действительны, но мы не можем делать такое предположение о любом произвольном сыром указателе.

Чтобы продемонстрировать это, далее мы создадим сырой указатель, в достоверности которого мы не можем быть так уверены, используя ключевое слово as для приведения значения вместо использования оператора сырого заимствования. В листинге 20-2 показано, как создать сырой указатель на произвольное место в памяти. Попытка использования произвольной памяти является неопределённым поведением: по этому адресу могут быть данные или нет, компилятор может оптимизировать код так, что доступ к памяти не произойдёт, или программа может завершиться с ошибкой сегментации. Обычно нет веской причины писать такой код, особенно в случаях, когда вместо этого можно использовать оператор сырого заимствования, но это возможно.

#![allow(unused)]
fn main() {
let address = 0x012345usize;
let r = address as *const i32;
}

Листинг 20-2: Создание сырого указателя на произвольный адрес памяти

Напомним, что мы можем создавать сырые указатели в безопасном коде, но мы не можем разыменовывать сырые указатели и читать данные, на которые они указывают. В листинге 20-3 мы используем оператор разыменования * на сыром указателе, что требует небезопасного блока.

#![allow(unused)]
fn main() {
let mut num = 5;

let r1 = &raw const num;
let r2 = &raw mut num;

unsafe {
    println!("r1 is: {}", *r1);
    println!("r2 is: {}", *r2);
}
}

Листинг 20-3: Разыменование сырых указателей внутри небезопасного блока

Создание указателя не приносит вреда; только когда мы пытаемся получить доступ к значению, на которое он указывает, мы можем столкнуться с недопустимым значением.

Также обратите внимание, что в листингах 20-1 и 20-3 мы создали сырые указатели *const i32 и *mut i32, которые оба указывают на одно и то же место в памяти, где хранится num. Если бы мы попытались создать неизменяемую и изменяемую ссылку на num, код не скомпилировался бы, потому что правила владения Rust не позволяют изменяемую ссылку одновременно с любыми неизменяемыми ссылками. С сырыми указателями мы можем создать изменяемый указатель и неизменяемый указатель на одно и то же место и изменять данные через изменяемый указатель, потенциально создавая состояние гонки данных (data race). Будьте осторожны!

Со всеми этими опасностями, зачем вообще использовать сырые указатели? Один из основных случаев использования — при взаимодействии с кодом на C, как вы увидите в следующем разделе. Другой случай — при построении безопасных абстракций, которые проверщик заимствований не понимает. Мы представим небезопасные функции, а затем рассмотрим пример безопасной абстракции, которая использует небезопасный код.

Вызов небезопасной функции или метода

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

Вот небезопасная функция с именем dangerous, которая ничего не делает в своём теле:

#![allow(unused)]
fn main() {
unsafe fn dangerous() {}

unsafe {
    dangerous();
}
}

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

$ cargo run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0133]: call to unsafe function `dangerous` is unsafe and requires unsafe block
 --> src/main.rs:4:5
  |
4 |     dangerous();
  |     ^^^^^^^^^^^ call to unsafe function
  |
  = note: consult the function's documentation for information on how to avoid undefined behavior

For more information about this error, try `rustc --explain E0133`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error

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

Для выполнения небезопасных операций в теле небезопасной функции вам всё равно нужно использовать небезопасный блок, так же как в обычной функции, и компилятор предупредит вас, если вы забудете. Это помогает нам сохранять небезопасные блоки как можно меньше, поскольку небезопасные операции могут быть нужны не во всём теле функции.

Создание безопасной абстракции над небезопасным кодом

Тот факт, что функция содержит небезопасный код, не означает, что нам нужно помечать всю функцию как unsafe. На самом деле, обёртывание небезопасного кода в безопасную функцию является распространённой абстракцией. В качестве примера давайте изучим функцию split_at_mut из стандартной библиотеки, которая требует некоторого небезопасного кода. Мы исследуем, как мы могли бы её реализовать. Этот безопасный метод определён для изменяемых срезов: он берёт один срез и разделяет его на два, разбивая срез по индексу, заданному в качестве аргумента. В листинге 20-4 показано, как использовать split_at_mut.

#![allow(unused)]
fn main() {
let mut v = vec![1, 2, 3, 4, 5, 6];

let r = &mut v[..];

let (a, b) = r.split_at_mut(3);

assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);
}

Листинг 20-4: Использование безопасной функции split_at_mut

Мы не можем реализовать эту функцию, используя только безопасный Rust. Попытка может выглядеть примерно как в листинге 20-5, который не скомпилируется. Для простоты мы реализуем split_at_mut как функцию, а не метод, и только для срезов значений i32, а не для обобщённого типа T.

#![allow(unused)]
fn main() {
// [Этот код не компилируется!]
fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();
    
    assert!(mid <= len);
    
    (&mut values[..mid], &mut values[mid..])
}
}

Листинг 20-5: Попытка реализации split_at_mut с использованием только безопасного Rust

Эта функция сначала получает общую длину среза. Затем она проверяет, что индекс, заданный в качестве параметра, находится within среза, проверяя, меньше ли он или равен длине. Проверка означает, что если мы передадим индекс, превышающий длину для разделения среза, функция вызовет панику до попытки использования этого индекса.

Затем мы возвращаем два изменяемых среза в кортеже: один от начала исходного среза до индекса mid и другой от mid до конца среза.

Когда мы пытаемся скомпилировать код из листинга 20-5, мы получаем ошибку:

$ cargo run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
error[E0499]: cannot borrow `*values` as mutable more than once at a time
 --> src/main.rs:6:31
  |
1 | fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
  |                         - let's call the lifetime of this reference `'1`
...
6 |     (&mut values[..mid], &mut values[mid..])
  |     --------------------------^^^^^^--------
  |     |     |                   |
  |     |     |                   second mutable borrow occurs here
  |     |     first mutable borrow occurs here
  |     returning this value requires that `*values` is borrowed for `'1`
  |
  = help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices

For more information about this error, try `rustc --explain E0499`.
error: could not compile `unsafe-example` (bin "unsafe-example") due to 1 previous error

Проверщик заимствований Rust не может понять, что мы заимствуем разные части среза; он знает только, что мы заимствуем из одного и того же среза дважды. Заимствование разных частей среза fundamentally допустимо, потому что два среза не перекрываются, но Rust недостаточно умен, чтобы это знать. Когда мы знаем, что код корректен, но Rust — нет, настало время обратиться к небезопасному коду.

В листинге 20-6 показано, как использовать небезопасный блок, сырой указатель и некоторые вызовы небезопасных функций, чтобы реализовать split_at_mut.

#![allow(unused)]
fn main() {
use std::slice;

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    let len = values.len();
    let ptr = values.as_mut_ptr();
    
    assert!(mid <= len);
    
    unsafe {
        (
            slice::from_raw_parts_mut(ptr, mid),
            slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}
}

Листинг 20-6: Использование небезопасного кода в реализации функции split_at_mut

Вспомните из раздела «Тип срез» в главе 4, что срез — это указатель на некоторые данные и длина среза. Мы используем метод len для получения длины среза и метод as_mut_ptr для доступа к сырому указателю среза. В этом случае, поскольку у нас есть изменяемый срез значений i32, as_mut_ptr возвращает сырой указатель типа *mut i32, который мы сохранили в переменной ptr.

Мы сохраняем проверку, что индекс mid находится within среза. Затем мы переходим к небезопасному коду: функция slice::from_raw_parts_mut принимает сырой указатель и длину и создаёт срез. Мы используем эту функцию для создания среза, который начинается с ptr и имеет длину mid элементов. Затем мы вызываем метод add на ptr с аргументом mid, чтобы получить сырой указатель, начинающийся с mid, и создаём срез, используя этот указатель и оставшееся количество элементов после mid в качестве длины.

Функция slice::from_raw_parts_mut является небезопасной, потому что она принимает сырой указатель и должна доверять, что этот указатель действителен. Метод add для сырых указателей также небезопасен, потому что он должен доверять, что смещённое расположение также является действительным указателем. Поэтому нам пришлось поместить небезопасный блок вокруг наших вызовов slice::from_raw_parts_mut и add, чтобы мы могли их вызвать. Глядя на код и добавляя проверку, что mid должен быть меньше или равен len, мы можем сказать, что все сырые указатели, используемые в небезопасном блоке, будут действительными указателями на данные within среза. Это допустимое и уместное использование unsafe.

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

В отличие от этого, использование slice::from_raw_parts_mut в листинге 20-7, вероятно, приведёт к сбою при использовании среза. Этот код берёт произвольное место в памяти и создаёт срез длиной 10 000 элементов.

#![allow(unused)]
fn main() {
use std::slice;

let address = 0x01234usize;
let r = address as *mut i32;

let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
}

Листинг 20-7: Создание среза из произвольного места в памяти

Мы не владеем памятью по этому произвольному адресу, и нет гарантии, что срез, созданный этим кодом, содержит действительные значения i32. Попытка использовать values как будто это действительный срез приводит к неопределённому поведению.

Использование внешних функций для вызова внешнего кода

Иногда вашему коду на Rust может потребоваться взаимодействовать с кодом, написанным на другом языке. Для этого в Rust есть ключевое слово extern, которое облегчает создание и использование внешнего функционального интерфейса (Foreign Function Interface, FFI) — это способ для языка программирования определять функции и позволять другому (внешнему) языку программирования вызывать эти функции.

В листинге 20-8 демонстрируется, как настроить интеграцию с функцией abs из стандартной библиотеки C. Функции, объявленные внутри блоков extern, как правило, небезопасно вызывать из кода Rust, поэтому блоки extern также должны быть помечены как unsafe. Причина в том, что другие языки не применяют правила и гарантии Rust, и Rust не может их проверить, поэтому ответственность за обеспечение безопасности ложится на программиста.

Файл: src/main.rs

unsafe extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}

Листинг 20-8: Объявление и вызов внешней функции, определённой на другом языке

Внутри блока unsafe extern "C" мы перечисляем имена и сигнатуры внешних функций из другого языка, которые хотим вызывать. Часть "C" определяет, какой двоичный интерфейс приложения (application binary interface, ABI) использует внешняя функция: ABI определяет, как вызывать функцию на уровне ассемблера. ABI "C" является наиболее распространённым и следует ABI языка программирования C. Информация обо всех ABI, которые поддерживает Rust, доступна в Rust Reference.

Каждый элемент, объявленный внутри блока unsafe extern, неявно является небезопасным. Однако некоторые FFI-функции безопасно вызывать. Например, функция abs из стандартной библиотеки C не имеет каких-либо соображений безопасности памяти, и мы знаем, что её можно вызывать с любым i32. В таких случаях мы можем использовать ключевое слово safe, чтобы указать, что эту конкретную функцию безопасно вызывать, даже если она находится в небезопасном блоке extern. После этого изменения её вызов больше не требует небезопасного блока, как показано в листинге 20-9.

Файл: src/main.rs

unsafe extern "C" {
    safe fn abs(input: i32) -> i32;
}

fn main() {
    println!("Absolute value of -3 according to C: {}", abs(-3));
}

Листинг 20-9: Явное помечение функции как безопасной внутри небезопасного блока extern и её безопасный вызов

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

Вызов функций Rust из других языков

Мы также можем использовать extern для создания интерфейса, который позволяет другим языкам вызывать функции Rust. Вместо создания целого блока extern мы добавляем ключевое слово extern и указываем ABI непосредственно перед ключевым словом fn для соответствующей функции. Нам также нужно добавить аннотацию #[unsafe(no_mangle)], чтобы сообщить компилятору Rust не искажать имя этой функции. Искажение имён (mangling) — это когда компилятор изменяет данное нами имя функции на другое имя, которое содержит больше информации для использования другими частями процесса компиляции, но является менее читаемым для человека.

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

В следующем примере мы делаем функцию call_from_c доступной для вызова из кода на C после её компиляции в shared library и линковки из C:

#![allow(unused)]
fn main() {
#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}
}

Это использование extern требует unsafe только в атрибуте, а не в блоке extern.

Доступ или изменение изменяемой статической переменной

В этой книге мы ещё не говорили о глобальных переменных, которые Rust поддерживает, но которые могут быть проблематичными с точки зрения правил владения Rust. Если два потока обращаются к одной и той же изменяемой глобальной переменной, это может вызвать состояние гонки данных (data race).

В Rust глобальные переменные называются статическими переменными (static variables). В листинге 20-10 показан пример объявления и использования статической переменной со строковым срезом в качестве значения.

Файл: src/main.rs

static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("value is: {HELLO_WORLD}");
}

Листинг 20-10: Определение и использование неизменяемой статической переменной

Статические переменные похожи на константы, которые мы обсуждали в разделе «Объявление констант» в главе 3. Имена статических переменных по соглашению записываются в SCREAMING_SNAKE_CASE. Статические переменные могут хранить только ссылки с временем жизни 'static, что означает, что компилятор Rust может определить время жизни самостоятельно, и нам не требуется аннотировать его явно. Доступ к неизменяемой статической переменной является безопасным.

Тонкое различие между константами и неизменяемыми статическими переменными заключается в том, что значения в статической переменной имеют фиксированный адрес в памяти. Использование значения всегда будет обращаться к одним и тем же данным. Константы, с другой стороны, могут дублировать свои данные при каждом использовании. Другое отличие состоит в том, что статические переменные могут быть изменяемыми. Доступ и изменение изменяемых статических переменных является небезопасным. В листинге 20-11 показано, как объявить, получить доступ и изменить изменяемую статическую переменную с именем COUNTER.

Файл: src/main.rs

static mut COUNTER: u32 = 0;

/// БЕЗОПАСНОСТЬ: Вызов этой функции более чем из одного потока одновременно
/// является неопределённым поведением, поэтому вы *должны* гарантировать,
/// что вызываете её только из одного потока за раз.
unsafe fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    unsafe {
        // БЕЗОПАСНОСТЬ: Эта функция вызывается только из одного потока в `main`.
        add_to_count(3);
        println!("COUNTER: {}", *(&raw const COUNTER));
    }
}

Листинг 20-11: Чтение или запись в изменяемую статическую переменную является небезопасным

Как и с обычными переменными, мы указываем изменяемость с помощью ключевого слова mut. Любой код, который читает или записывает в COUNTER, должен находиться в небезопасном блоке. Код в листинге 20-11 компилируется и выводит COUNTER: 3, как мы и ожидали, потому что он однопоточный. Если бы несколько потоков обращались к COUNTER, это, вероятно, привело бы к состоянию гонки данных, что является неопределённым поведением. Поэтому нам нужно пометить всю функцию как unsafe и задокументировать ограничения безопасности, чтобы любой, кто вызывает функцию, знал, что можно и что нельзя делать безопасно.

Каждый раз, когда мы пишем небезопасную функцию, принято писать комментарий, начинающийся с БЕЗОПАСНОСТЬ и объясняющий, что вызывающая сторона должна сделать для безопасного вызова функции. Аналогично, при выполнении небезопасной операции принято писать комментарий, начинающийся с БЕЗОПАСНОСТЬ, чтобы объяснить, как соблюдаются правила безопасности.

Кроме того, компилятор по умолчанию запрещает любые попытки создания ссылок на изменяемую статическую переменную через линту компилятора. Вы должны либо явно отказаться от защиты этой линты, добавив аннотацию #[allow(static_mut_refs)], либо обращаться к изменяемой статической переменной через сырой указатель, созданный с помощью одного из операторов сырого заимствования. Это включает случаи, когда ссылка создаётся неявно, как при использовании в макросе println! в этом примере кода. Требование создавать ссылки на статические изменяемые переменные через сырые указатели помогает сделать требования безопасности для их использования более очевидными.

При работе с изменяемыми данными, которые доступны глобально, трудно обеспечить отсутствие состояний гонки данных, поэтому Rust считает изменяемые статические переменные небезопасными. По возможности предпочтительнее использовать методы конкурентности и потокобезопасные умные указатели, которые мы обсуждали в главе 16, чтобы компилятор проверял, что доступ к данным из разных потоков осуществляется безопасно.

Реализация небезопасного трейта

Мы можем использовать unsafe для реализации небезопасного трейта. Трейт является небезопасным, когда по крайней мере один из его методов имеет некоторую инварианту, которую компилятор не может проверить. Мы объявляем трейт небезопасным, добавляя ключевое слово unsafe перед trait, и также помечаем реализацию этого трейта как unsafe, как показано в листинге 20-12.

#![allow(unused)]
fn main() {
unsafe trait Foo {
    // методы здесь
}

unsafe impl Foo for i32 {
    // реализации методов здесь
}
}

Листинг 20-12: Определение и реализация небезопасного трейта

Используя unsafe impl, мы гарантируем, что будем соблюдать инварианты, которые компилятор не может проверить.

В качестве примера вспомните маркерные трейты Send и Sync, которые мы обсуждали в разделе «Расширяемая конкурентность с Send и Sync» в главе 16: компилятор автоматически реализует эти трейты, если наши типы полностью состоят из других типов, которые реализуют Send и Sync. Если мы реализуем тип, который содержит тип, не реализующий Send или Sync, такой как сырые указатели, и мы хотим пометить этот тип как Send или Sync, мы должны использовать unsafe. Rust не может проверить, что наш тип соблюдает гарантии того, что его можно безопасно передавать между потоками или обращаться к нему из нескольких потоков; следовательно, нам нужно выполнить эти проверки вручную и указать это с помощью unsafe.

Доступ к полям объединения (Union)

Последнее действие, которое работает только с unsafe, — это доступ к полям объединения (union). Объединение похоже на структуру, но в конкретном экземпляре в один момент времени используется только одно объявленное поле. Объединения в основном используются для взаимодействия с объединениями в коде C. Доступ к полям объединения небезопасен, потому что Rust не может гарантировать тип данных, хранящихся в данный момент в экземпляре объединения. Вы можете узнать больше об объединениях в Rust Reference.

Использование Miri для проверки небезопасного кода

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

Использование Miri требует ночной сборки Rust (о которой мы подробнее рассказываем в Приложении G: «Как создаётся Rust и „Ночной Rust“»). Вы можете установить как ночную версию Rust, так и инструмент Miri, набрав rustup +nightly component add miri. Это не изменяет версию Rust, которую использует ваш проект; это только добавляет инструмент в вашу систему, чтобы вы могли использовать его, когда захотите. Вы можете запустить Miri для проекта, набрав cargo +nightly miri run или cargo +nightly miri test.

В качестве примера полезности этого инструмента рассмотрим, что происходит, когда мы запускаем его для листинга 20-7.

$ cargo +nightly miri run
   Compiling unsafe-example v0.1.0 (file:///projects/unsafe-example)
    Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.01s
     Running `file:///home/.rustup/toolchains/nightly/bin/cargo-miri runner target/miri/debug/unsafe-example`
warning: integer-to-pointer cast
 --> src/main.rs:5:13
  |
5 |     let r = address as *mut i32;
  |             ^^^^^^^^^^^^^^^^^^^ integer-to-pointer cast
  |
  = help: this program is using integer-to-pointer casts or (equivalently) `ptr::with_exposed_provenance`, which means that Miri might miss pointer bugs in this program
  = help: see https://doc.rust-lang.org/nightly/std/ptr/fn.with_exposed_provenance.html for more details on that operation
  = help: to ensure that Miri does not miss bugs in your program, use Strict Provenance APIs (https://doc.rust-lang.org/nightly/std/ptr/index.html#strict-provenance, https://crates.io/crates/sptr) instead
  = help: you can then set `MIRIFLAGS=-Zmiri-strict-provenance` to ensure you are not relying on `with_exposed_provenance` semantics
  = note: BACKTRACE:
  = note: inside `main` at src/main.rs:5:13: 5:32

error: Undefined Behavior: pointer not dereferenceable: pointer must be dereferenceable for 40000 bytes, but got 0x1234[noalloc] which is a dangling pointer (it has no provenance)
 --> src/main.rs:7:35
  |
7 |     let values: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
  |                                   ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
  |
  = help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
  = help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
  = note: BACKTRACE:
  = note: inside `main` at src/main.rs:7:35: 7:70

note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace

error: aborting due to 1 previous error; 1 warning emitted

Miri правильно предупреждает нас, что мы преобразуем целое число в указатель, что может быть проблемой, но Miri не может определить, существует ли проблема, потому что он не знает, как возник указатель. Затем Miri возвращает ошибку, где листинг 20-7 имеет неопределённое поведение, потому что у нас висячий указатель (dangling pointer). Благодаря Miri мы теперь знаем, что существует риск неопределённого поведения, и можем подумать о том, как сделать код безопасным. В некоторых случаях Miri может даже давать рекомендации по исправлению ошибок.

Miri не обнаруживает всё, что может быть неправильно в небезопасном коде. Miri — это инструмент динамического анализа, поэтому он обнаруживает только проблемы в коде, который фактически выполняется. Это означает, что вам нужно использовать его в сочетании с хорошими методами тестирования, чтобы повысить уверенность в написанном небезопасном коде. Miri также не охватывает все возможные способы, которыми ваш код может быть некорректным.

Другими словами: если Miri обнаруживает проблему, вы знаете, что есть ошибка, но если Miri не обнаруживает ошибку, это не значит, что проблемы нет. Тем не менее, он может обнаружить многое. Попробуйте запустить его на других примерах небезопасного кода в этой главе и посмотрите, что он скажет!

Вы можете узнать больше о Miri в его репозитории на GitHub.

Правильное использование небезопасного кода

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

Для более глубокого изучения эффективной работы с небезопасным Rust прочитайте официальное руководство Rust по небезопасному коду — The Rustonomicon.