Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Unsafe Rust

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

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

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

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

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

  1. Разыменовывать сырой указатель.
  2. Вызывать unsafe-функцию или unsafe-метод.
  3. Получать доступ к изменяемой статической переменной или изменять ее.
  4. Реализовывать unsafe-трейт.
  5. Получать доступ к полям union.

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

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

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

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

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

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

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

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

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

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

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

fn main() {
    let mut num = 5;

    let r1 = &raw const num;
    let r2 = &raw mut num;
}
Listing 20-1: Создание сырых указателей с помощью операторов сырого заимствования

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

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

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

fn main() {
    let address = 0x012345usize;
    let r = address as *const i32;
}
Listing 20-2: Создание сырого указателя на произвольный адрес памяти

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

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);
    }
}
Listing 20-3: Разыменование сырых указателей внутри блока unsafe

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

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

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

Вызов unsafe-функции или метода

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

Вот unsafe-функция с именем dangerous, которая ничего не делает в своем теле:

fn main() {
    unsafe fn dangerous() {}

    unsafe {
        dangerous();
    }
}

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

$ 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

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

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

Создание безопасной абстракции поверх unsafe-кода

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

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]);
}
Listing 20-4: Использование безопасной функции split_at_mut

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

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..])
}

fn main() {
    let mut vector = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut vector, 3);
}
Listing 20-5: Попытка реализовать split_at_mut только с помощью безопасного Rust

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

Затем мы возвращаем два изменяемых среза в кортеже: один от начала исходного среза до индекса 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 не может понять, что мы заимствуем разные части среза; он знает только, что мы дважды заимствуем из одного и того же среза. Заимствовать разные части среза в принципе нормально, потому что эти два среза не пересекаются, но Rust недостаточно умен, чтобы это знать. Когда мы знаем, что код корректен, а Rust — нет, пора обратиться к unsafe-коду.

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

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),
        )
    }
}

fn main() {
    let mut vector = vec![1, 2, 3, 4, 5, 6];
    let (left, right) = split_at_mut(&mut vector, 3);
}
Listing 20-6: Использование unsafe-кода в реализации функции split_at_mut

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

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

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

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

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

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) };
}
Listing 20-7: Создание среза из произвольного места в памяти

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

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

Иногда вашему 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));
    }
}
Listing 20-8: Объявление и вызов функции extern, определенной в другом языке

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

Каждый элемент, объявленный внутри блока unsafe extern, неявно является unsafe. Однако некоторые FFI-функции безопасны для вызова. Например, функция abs из стандартной библиотеки C не имеет соображений безопасности памяти, и мы знаем, что ее можно вызвать с любым i32. В таких случаях можно использовать ключевое слово safe, чтобы сказать, что эта конкретная функция безопасна для вызова, хотя находится в блоке unsafe extern. После такого изменения ее вызов больше не требует блока unsafe, как показано в листинге 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));
}
Listing 20-9: Явная пометка функции как safe внутри блока unsafe extern и ее безопасный вызов

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

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

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

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

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

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

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

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

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

Имя файла: src/main.rs
static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("value is: {HELLO_WORLD}");
}
Listing 20-10: Определение и использование неизменяемой статической переменной

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

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

Имя файла: src/main.rs
static mut COUNTER: u32 = 0;

/// SAFETY: Calling this from more than a single thread at a time is undefined
/// behavior, so you *must* guarantee you only call it from a single thread at
/// a time.
unsafe fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    unsafe {
        // SAFETY: This is only called from a single thread in `main`.
        add_to_count(3);
        println!("COUNTER: {}", *(&raw const COUNTER));
    }
}
Listing 20-11: Чтение из изменяемой статической переменной или запись в нее небезопасны.

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

Каждый раз, когда мы пишем unsafe-функцию, идиоматично писать комментарий, начинающийся с SAFETY, и объяснять, что вызывающая сторона должна сделать, чтобы безопасно вызвать функцию. Аналогично, каждый раз, когда мы выполняем unsafe-операцию, идиоматично писать комментарий, начинающийся с SAFETY, чтобы объяснить, как соблюдаются правила безопасности.

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

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

Реализация unsafe-трейта

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

unsafe trait Foo {
    // methods go here
}

unsafe impl Foo for i32 {
    // method implementations go here
}

fn main() {}
Listing 20-12: Определение и реализация unsafe-трейта

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

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

Доступ к полям union

Последнее действие, которое работает только с unsafe, — доступ к полям union. Union похож на struct, но в конкретном экземпляре в один момент времени используется только одно объявленное поле. Конструкции union в основном используются для взаимодействия с union в коде C. Доступ к полям union небезопасен, потому что Rust не может гарантировать тип данных, которые сейчас хранятся в экземпляре union. Подробнее о union можно узнать в Rust Reference.

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

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

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

Чтобы увидеть, насколько это может быть полезно, рассмотрим, что происходит при запуске Miri для листинга 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
  = help: alternatively, `MIRIFLAGS=-Zmiri-permissive-provenance` disables this warning
  = 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 есть неопределенное поведение из-за висячего указателя. Благодаря Miri теперь мы знаем, что существует риск неопределенного поведения, и можем подумать, как сделать код безопасным. В некоторых случаях Miri даже может давать рекомендации по исправлению ошибок.

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

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

Подробнее о Miri можно узнать в его репозитории GitHub.

Правильное использование unsafe-кода

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

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