Небезопасный Rust
Во всём коде, который мы обсуждали до сих пор, гарантии безопасности памяти Rust проверялись при компиляции. Но внутри Rust спрятан второй язык, который этих гарантий не обеспечивает. Он называется небезопасным Rust (unsafe Rust) и работает как обычный Rust, но даёт дополнительные «суперспособности».
Небезопасный Rust существует потому, что статический анализ по своей природе консервативен. Когда компилятор пытается определить, соблюдает ли код гарантии, лучше отвергнуть какие-то корректные программы, чем принять некорректные. Код может быть в порядке, но если компилятору Rust не хватает информации, чтобы в этом убедиться, он код отвергнет. В таких случаях небезопасным кодом можно сказать компилятору: «Поверь, я знаю, что делаю». Но учтите: небезопасный Rust вы используете на свой страх и риск. Если использовать его неправильно, возможны проблемы из-за небезопасной работы с памятью — например, разыменование нулевого указателя.
Есть и другая причина, по которой у Rust есть небезопасное альтер эго: само железо компьютера по своей природе небезопасно. Если бы Rust не позволял небезопасных операций, некоторые задачи было бы не решить. Rust должен позволять низкоуровневое системное программирование — например, напрямую взаимодействовать с операционной системой или даже писать собственную ОС. Низкоуровневое системное программирование — одна из целей языка. Посмотрим, что и как можно делать с помощью небезопасного Rust.
Небезопасные суперспособности
Чтобы перейти к небезопасному Rust, напишите ключевое слово unsafe и откройте
новый блок с небезопасным кодом. В небезопасном Rust можно делать пять вещей,
недоступных в безопасном; мы называем их небезопасными суперспособностями
(unsafe superpowers). Вот они:
- Разыменовать сырой указатель.
- Вызвать небезопасную функцию или метод.
- Обратиться к изменяемой статической переменной или изменить её.
- Реализовать небезопасный трейт.
- Обратиться к полям объединения (
union).
Важно понимать, что unsafe не отключает анализатор заимствований и другие
проверки безопасности Rust: если использовать ссылку в небезопасном коде, она
всё равно будет проверена. Ключевое слово unsafe лишь открывает доступ к этим
пяти возможностям, которые компилятор затем не проверяет на безопасность
памяти. Определённая степень безопасности внутри блока unsafe всё равно
сохраняется.
Кроме того, unsafe не означает, что код внутри блока обязательно опасен или в
нём точно будут проблемы с безопасностью памяти. Смысл в том, что вы как
программист сами гарантируете, что код внутри блока unsafe обращается к
памяти корректно.
Люди ошибаются, и ошибки будут. Но раз эти пять небезопасных операций
разрешены только внутри блоков, помеченных unsafe, вы будете знать: любые
ошибки, связанные с безопасностью памяти, находятся внутри какого-то блока
unsafe. Делайте блоки unsafe маленькими — потом, разбираясь с ошибками
памяти, вы скажете себе спасибо.
Чтобы как можно сильнее изолировать небезопасный код, лучше всего обернуть его
в безопасную абстракцию с безопасным API — об этом мы поговорим дальше в главе,
когда будем разбирать небезопасные функции и методы. Части стандартной
библиотеки реализованы как безопасные абстракции над проверенным небезопасным
кодом. Обёртка из безопасной абстракции не даёт unsafe расползтись по всем
местам, где вы или ваши пользователи захотите воспользоваться
функциональностью, реализованной на небезопасном коде, — ведь пользоваться
безопасной абстракцией безопасно.
Разберём по очереди все пять небезопасных суперспособностей, а заодно посмотрим на абстракции, которые дают безопасный интерфейс к небезопасному коду.
Разыменование сырого указателя
В главе 4, в разделе «Висячие ссылки», мы
говорили, что компилятор гарантирует действительность ссылок. В небезопасном
Rust есть два новых типа, похожих на ссылки, — сырые указатели (raw
pointers). Как и ссылки, сырые указатели бывают неизменяемыми и изменяемыми и
записываются как *const T и *mut T соответственно. Звёздочка здесь — не
оператор разыменования, а часть имени типа. Применительно к сырым указателям
неизменяемый означает, что после разыменования указателю нельзя напрямую
присвоить значение.
В отличие от ссылок и умных указателей, сырые указатели:
- могут нарушать правила заимствования: можно иметь одновременно неизменяемые и изменяемые указатели или несколько изменяемых указателей на одно и то же место;
- не обязательно указывают на действительную память;
- могут быть нулевыми;
- не реализуют никакой автоматической очистки.
Отказываясь от этих гарантий Rust, вы жертвуете гарантированной безопасностью ради большей производительности или возможности взаимодействовать с другим языком или железом, где гарантии Rust не действуют.
В листинге 20-1 показано, как создать неизменяемый и изменяемый сырые указатели.
fn main() {
let mut num = 5;
let r1 = &raw const num;
let r2 = &raw mut num;
}
Заметьте, что ключевого слова 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;
}
Напомним: создавать сырые указатели в безопасном коде можно, а вот
разыменовывать их и читать данные, на которые они указывают, — нельзя. В
листинге 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);
}
}
unsafeСоздание указателя ничем не вредит; проблемы с недействительным значением могут возникнуть, только когда мы пытаемся обратиться к тому, на что он указывает.
Обратите также внимание: в листингах 20-1 и 20-3 мы создали сырые указатели
*const i32 и *mut i32, которые оба указывают на одно и то же место в памяти —
туда, где хранится num. Если бы мы попытались создать неизменяемую и
изменяемую ссылки на num, код не скомпилировался бы: правила владения Rust не
допускают изменяемую ссылку одновременно с неизменяемыми. А с сырыми
указателями можно создать изменяемый и неизменяемый указатели на одно и то же
место и менять данные через изменяемый — и, возможно, получить гонку данных.
Будьте осторожны!
При всех этих опасностях зачем вообще использовать сырые указатели? Одно из главных применений — взаимодействие с кодом на C, как вы увидите в следующем разделе. Другое — построение безопасных абстракций, которых не понимает анализатор заимствований. Сначала познакомимся с небезопасными функциями, а потом посмотрим на пример безопасной абстракции на небезопасном коде.
Вызов небезопасной функции или метода
Второй вид операций, доступных в блоке unsafe, — вызов небезопасных функций.
Небезопасные функции и методы выглядят точно так же, как обычные, но перед
определением у них стоит unsafe. Здесь ключевое слово unsafe означает, что
у функции есть требования, которые мы должны соблюдать при её вызове, потому
что Rust не может гарантировать их выполнение. Вызывая небезопасную функцию
внутри блока 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 как можно меньше: небезопасные
операции могут быть нужны не во всём теле функции.
Безопасная абстракция над небезопасным кодом
Если функция содержит небезопасный код, это ещё не значит, что всю функцию
нужно помечать как небезопасную. Наоборот, обернуть небезопасный код в
безопасную функцию — распространённая абстракция. Для примера разберём функцию
split_at_mut из стандартной библиотеки, которой нужен небезопасный код, и
посмотрим, как её можно реализовать. Этот безопасный метод определён для
изменяемых срезов: он берёт один срез и делает из него два, разделив его по
переданному индексу. В листинге 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]);
}
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);
}
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 — нет, пора браться за небезопасный код.
В листинге 20-6 показано, как с помощью блока 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);
}
split_at_mutВспомните из раздела «Срезы» главы 4: срез —
это указатель на данные и длина. Длину среза мы получаем методом len, а сырой
указатель среза — методом as_mut_ptr. Здесь у нас изменяемый срез значений
i32, поэтому as_mut_ptr возвращает сырой указатель типа *mut i32, который
мы сохраняем в переменной ptr.
Проверку, что индекс mid лежит внутри среза, мы оставляем. Дальше идёт
небезопасный код: функция slice::from_raw_parts_mut принимает сырой указатель
и длину и создаёт срез. С её помощью мы создаём срез, который начинается с
ptr и содержит mid элементов. Затем вызываем у ptr метод add с аргументом
mid, чтобы получить сырой указатель, начинающийся с mid, и создаём срез из
этого указателя и оставшегося после mid числа элементов.
Функция slice::from_raw_parts_mut небезопасна: она принимает сырой указатель
и вынуждена доверять, что он действителен. Метод add у сырых указателей тоже
небезопасен: он вынужден доверять, что место по смещению — тоже
действительный указатель. Поэтому вызовы slice::from_raw_parts_mut и add
пришлось обернуть в блок unsafe. Глядя на код и на проверку, что mid не
больше len, мы можем сказать, что все сырые указатели внутри блока unsafe
будут действительными указателями на данные внутри среза. Это приемлемое и
уместное использование unsafe.
Заметьте: помечать получившуюся функцию split_at_mut как unsafe не нужно, и
вызывать её можно из безопасного Rust. Мы создали безопасную абстракцию над
небезопасным кодом: реализация функции использует 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) };
}
Памятью по этому произвольному адресу мы не владеем, и нет никакой гарантии,
что созданный срез содержит корректные значения i32. Попытка использовать
values как корректный срез — неопределённое поведение.
Вызов внешнего кода через функции extern
Иногда коду на Rust нужно взаимодействовать с кодом на другом языке. Для этого
в Rust есть ключевое слово extern, которое помогает создавать и использовать
интерфейс внешних функций (Foreign Function Interface, FFI) — способ, которым
один язык программирования определяет функции и позволяет другому (внешнему)
языку их вызывать.
В листинге 20-8 показано, как подключить функцию abs из стандартной
библиотеки C. Функции, объявленные в блоках extern, вызывать из кода на Rust
обычно небезопасно, поэтому сами блоки extern тоже должны быть помечены
unsafe. Причина в том, что другие языки не соблюдают правил и гарантий Rust, а
Rust не может их проверить, так что ответственность за безопасность ложится на
программиста.
unsafe extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
extern, определённую в другом языкеВ блоке unsafe extern "C" перечисляются имена и сигнатуры внешних функций из
другого языка, которые мы хотим вызывать. Часть "C" задаёт, какой двоичный
интерфейс приложений (application binary interface, ABI) использует внешняя
функция: ABI определяет, как вызывать функцию на уровне ассемблера. ABI "C" —
самый распространённый и соответствует ABI языка C. Сведения обо всех ABI,
которые поддерживает Rust, есть в справочнике Rust.
Любой элемент, объявленный в блоке unsafe extern, неявно небезопасен. Но
некоторые FFI-функции вызывать безопасно. Например, у функции abs из
стандартной библиотеки C нет никаких проблем с безопасностью памяти, и мы
знаем, что её можно вызывать с любым i32. В таких случаях с помощью ключевого
слова safe можно сказать, что эта конкретная функция безопасна, хотя и
находится в блоке unsafe extern. После этого её вызов уже не требует блока
unsafe, как в листинге 20-9.
unsafe extern "C" {
safe fn abs(input: i32) -> i32;
}
fn main() {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
safe внутри блока unsafe extern и вызываем её безопасноПометка safe сама по себе не делает функцию безопасной! Это скорее ваше
обещание Rust, что она безопасна. Следить за тем, чтобы обещание выполнялось, —
по-прежнему ваша обязанность!
Вызов функций Rust из других языков
С помощью extern можно сделать и интерфейс, через который другие языки
вызывают функции Rust. Вместо целого блока extern ключевое слово extern и
нужный ABI указывают прямо перед ключевым словом fn у соответствующей
функции. Ещё нужно добавить аннотацию #[unsafe(no_mangle)], чтобы компилятор
Rust не искажал (mangle) имя этой функции. Искажение имён — это когда
компилятор меняет данное нами имя функции на другое: в нём больше информации
для других этапов компиляции, но человеку его читать труднее. Компилятор
каждого языка искажает имена по-своему, поэтому, чтобы функцию 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 глобальные переменные называются статическими (static). В листинге 20-10 — пример объявления и использования статической переменной со значением — строковым срезом.
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("value is: {HELLO_WORLD}");
}
Статические переменные похожи на константы, о которых мы говорили в разделе
«Константы» главы 3. Имена статических переменных по
соглашению пишутся в SCREAMING_SNAKE_CASE. Статические переменные могут
хранить только ссылки со временем жизни 'static, а значит, компилятор Rust сам
может определить время жизни, и указывать его явно не нужно. Обращение к
неизменяемой статической переменной безопасно.
Тонкое отличие констант от неизменяемых статических переменных в том, что у
значения статической переменной фиксированный адрес в памяти: при каждом
использовании обращение идёт к одним и тем же данным. А константы могут
дублировать свои данные при каждом использовании. Ещё одно отличие —
статические переменные могут быть изменяемыми. Обращение к изменяемым
статическим переменным и их изменение — небезопасны. В листинге 20-11 показано,
как объявить изменяемую статическую переменную COUNTER, обращаться к ней и
менять её.
static mut COUNTER: u32 = 0;
/// SAFETY: вызов этой функции одновременно из нескольких потоков — неопределённое
/// поведение, поэтому вы *обязаны* гарантировать, что она вызывается только
/// из одного потока за раз.
unsafe fn add_to_count(inc: u32) {
unsafe {
COUNTER += inc;
}
}
fn main() {
unsafe {
// SAFETY: вызывается только из одного потока, в `main`.
add_to_count(3);
println!("COUNTER: {}", *(&raw const COUNTER));
}
}
Как и у обычных переменных, изменяемость задаётся ключевым словом mut. Любой
код, читающий COUNTER или пишущий в неё, должен быть внутри блока unsafe. Код
из листинга 20-11 компилируется и, как и ожидалось, выводит COUNTER: 3, потому
что он однопоточный. Если бы к COUNTER обращались несколько потоков, скорее
всего, возникли бы гонки данных, а это неопределённое поведение. Поэтому всю
функцию нужно пометить как unsafe и задокументировать ограничение
безопасности, чтобы любой, кто её вызывает, знал, что можно делать безопасно, а
что нельзя.
Когда мы пишем небезопасную функцию, идиоматично добавлять комментарий,
начинающийся с SAFETY, и объяснять в нём, что должен сделать вызывающий,
чтобы вызвать функцию безопасно. Точно так же, выполняя небезопасную операцию,
идиоматично писать комментарий с SAFETY и объяснять, как соблюдаются правила
безопасности.
Кроме того, компилятор с помощью линта по умолчанию запрещает любые попытки
создать ссылки на изменяемую статическую переменную. Нужно либо явно отказаться
от защиты этого линта, добавив аннотацию #[allow(static_mut_refs)], либо
обращаться к изменяемой статической переменной через сырой указатель,
созданный одним из операторов сырого заимствования. Это касается и случаев,
когда ссылка создаётся незаметно — как при использовании в println! в этом
листинге. Требование создавать ссылки на изменяемые статические переменные
через сырые указатели помогает сделать требования безопасности к ним
нагляднее.
С изменяемыми глобально доступными данными трудно гарантировать отсутствие гонок данных, поэтому Rust и считает изменяемые статические переменные небезопасными. Где возможно, лучше использовать приёмы конкурентности и потокобезопасные умные указатели из главы 16 — тогда компилятор проверит, что доступ к данным из разных потоков безопасен.
Реализация небезопасного трейта
С помощью unsafe можно реализовать небезопасный трейт. Трейт небезопасен,
если хотя бы у одного из его методов есть инвариант, который компилятор не
может проверить. Чтобы объявить трейт небезопасным, перед trait добавляют
ключевое слово unsafe, а реализацию трейта тоже помечают unsafe, как в
листинге 20-12.
unsafe trait Foo {
// здесь методы
}
unsafe impl Foo for i32 {
// здесь реализации методов
}
fn main() {}
Используя unsafe impl, мы обещаем соблюдать инварианты, которые компилятор
проверить не может.
Для примера вспомните трейты-маркеры Send и Sync из раздела «Расширяемая
конкурентность: Send и Sync» главы 16:
компилятор реализует их автоматически, если наши типы целиком состоят из
других типов, реализующих Send и Sync. Если же мы реализуем тип, в котором
есть тип без Send или Sync — например, сырые указатели, — и хотим пометить
наш тип как Send или Sync, нужно использовать unsafe. Rust не может
проверить, что наш тип соблюдает гарантии, позволяющие безопасно передавать
его между потоками или обращаться к нему из нескольких потоков, поэтому
проверять это приходится вручную и сообщать об этом через unsafe.
Доступ к полям объединения
Последнее, что можно сделать только с unsafe, — обратиться к полям
объединения. Объединение (union) похоже на структуру (struct), но в каждом
экземпляре в каждый момент используется лишь одно из объявленных полей.
Объединения нужны в основном для взаимодействия с объединениями в коде на C.
Обращение к полям объединения небезопасно, потому что Rust не может
гарантировать, данные какого типа сейчас хранятся в экземпляре объединения.
Подробнее об объединениях — в справочнике Rust.
Проверка небезопасного кода с помощью Miri
Когда небезопасный код написан, хочется проверить, что он действительно безопасен и корректен. Один из лучших способов — 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.
Чтобы увидеть, насколько это полезно, посмотрим, что будет, если запустить его для листинга 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.17s
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
error: Undefined Behavior: constructing invalid value of type &mut [i32]: encountered a dangling reference (0x1234[noalloc] 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: 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 ловит не всё, что можно сделать неправильно в небезопасном коде. Это инструмент динамического анализа, поэтому он находит проблемы только в коде, который реально выполняется. Значит, использовать его нужно вместе с хорошими практиками тестирования, чтобы увереннее полагаться на свой небезопасный код. И не все способы, которыми код может оказаться некорректным (unsound), Miri охватывает.
Иначе говоря: если Miri нашёл проблему, баг точно есть; но если Miri бага не нашёл, это ещё не значит, что проблемы нет. Хотя ловит он многое. Попробуйте запустить его на других примерах небезопасного кода из этой главы и посмотрите, что он скажет!
Подробнее о Miri — в его репозитории на GitHub.
Как правильно пользоваться небезопасным кодом
Использовать unsafe ради одной из пяти описанных суперспособностей — не
ошибка и даже не что-то порицаемое. Просто правильно написать unsafe-код
сложнее: компилятор не может помочь соблюсти безопасность памяти. Когда у вас
есть причина использовать небезопасный код, — используйте; явная пометка
unsafe поможет быстрее найти источник проблем, если они возникнут. А написав
небезопасный код, проверьте его с помощью Miri, чтобы увереннее знать, что он
соблюдает правила Rust.
Чтобы гораздо глубже разобраться, как эффективно работать с небезопасным Rust,
прочитайте официальное руководство Rust по unsafe — The Rustonomicon.