Небезопасный Rust
Весь код, который мы обсуждали до сих пор, имел гарантии безопасности памяти Rust, проверяемые на этапе компиляции. Однако внутри Rust существует второй язык, который не обеспечивает эти гарантии: он называется небезопасным Rust и работает так же, как обычный Rust, но даёт нам дополнительные сверхспособности.
Небезопасный Rust существует потому, что статический анализ по своей природе консервативен. Когда компилятор пытается определить, соблюдает ли код гарантии, лучше отвергнуть некоторые корректные программы, чем принять некорректные. Хотя код может быть правильным, если у компилятора Rust недостаточно информации для уверенности, он отклонит код. В таких случаях вы можете использовать небезопасный код, чтобы сказать компилятору: «Доверься, я знаю, что делаю». Однако предупреждаем: вы используете небезопасный Rust на свой страх и риск: если вы используете небезопасный код некорректно, могут возникнуть проблемы из-за нарушения безопасности памяти, такие как разыменование нулевого указателя.
Другая причина, по которой у Rust есть небезопасный alter ego, в том, что аппаратное обеспечение компьютера изначально небезопасно. Если бы Rust не позволял выполнять небезопасные операции, вы не смогли бы решать определённые задачи. Rust должен позволять вам заниматься низкоуровневым системным программированием, например, напрямую взаимодействовать с операционной системой или даже писать свою собственную ОС. Работа с низкоуровневым системным программированием — одна из целей языка. Давайте исследуем, что мы можем делать с небезопасным Rust и как это делать.
Сверхспособности небезопасного Rust
Чтобы переключиться на небезопасный Rust, используйте ключевое слово unsafe и затем начните новый блок, содержащий небезопасный код. Вы можете выполнять в небезопасном Rust пять действий, которые не можете в безопасном Rust, что мы называем сверхспособностями небезопасного Rust. Эти сверхспособности включают возможность:
- Разыменовывать сырой указатель
- Вызывать небезопасную функцию или метод
- Обращаться к изменяемой статической переменной или изменять её
- Реализовывать небезопасный типаж
- Обращаться к полям
union
Важно понимать, что unsafe не отключает проверку заимствований или другие проверки безопасности Rust: если вы используете ссылку в небезопасном коде, она всё равно будет проверяться. Ключевое слово unsafe только даёт доступ к этим пяти возможностям, которые затем не проверяются компилятором на безопасность памяти. Вы всё ещё получите некоторую степень безопасности внутри блока unsafe.
Кроме того, unsafe не означает, что код внутри блока обязательно опасен или что он определённо будет иметь проблемы с безопасностью памяти: предполагается, что как программист вы обеспечите доступ к памяти в блоке unsafe корректным способом.
Люди подвержены ошибкам, и ошибки случаются, но требуя, чтобы эти пять небезопасных операций были внутри блоков, аннотированных unsafe, вы будете знать, что любые ошибки, связанные с безопасностью памяти, должны находиться внутри блока unsafe. Делайте блоки unsafe небольшими; вы будете благодарны позже, когда будете исследовать ошибки памяти.
Чтобы изолировать небезопасный код как можно больше, лучше всего заключать такой код внутри безопасной абстракции и предоставлять безопасный API, что мы обсудим позже в этой главе, когда рассмотрим небезопасные функции и методы. Части стандартной библиотеки реализованы как безопасные абстракции над небезопасным кодом, который прошёл аудит. Обёртывание небезопасного кода в безопасную абстракцию предотвращает «просачивание» использования unsafe во все места, где вы или ваши пользователи могли бы захотеть использовать функциональность, реализованную с помощью небезопасного кода, поскольку использование безопасной абстракции безопасно.
Давайте рассмотрим каждую из пяти сверхспособностей небезопасного Rust по очереди. Мы также посмотрим на некоторые абстракции, которые предоставляют безопасный интерфейс для небезопасного кода.
Разыменование сырого указателя
В разделе «Проверка прав доступа» главы 4 мы описали, как компилятор обеспечивает, чтобы ссылки всегда были корректными. Небезопасный 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; }
Обратите внимание, что мы не включаем ключевое слово 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 как можно меньшими, поскольку небезопасные операции могут быть не нужны во всём теле функции.
Создание безопасной абстракции над небезопасным кодом
То, что функция содержит небезопасный код, не означает, что нам нужно помечать всю функцию как небезопасную. На самом деле, обёртывание небезопасного кода в безопасную функцию — распространённая абстракция. В качестве примера изучим функцию 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 элементов. Затем мы вызываем метод 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. Мы создали безопасную абстракцию для небезопасного кода с реализацией функции, которая использует небезопасный код безопасным способом, потому что она создаёт только действительные указатели из данных, к которым эта функция имеет доступ.
В отличие от этого, использование 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, которое облегчает создание и использование внешнего интерфейса функций (FFI). 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" определяет, какой бинарный интерфейс приложения (ABI) использует внешняя функция: ABI определяет, как вызывать функцию на уровне сборки. ABI "C" — самый распространённый и следует ABI языка программирования C. Информация обо всех ABI, которые поддерживает Rust, доступна в справочнике Rust.
Каждый элемент, объявленный внутри блока unsafe extern, подразумевается unsafe. Однако некоторые функции 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 не выполнять манглирование имени этой функции. Манглирование — это когда компилятор изменяет имя, которое мы дали функции, на другое имя, содержащее больше информации для потребления другими частями процесса компиляции, но менее читаемое человеком. Каждый компилятор языка программирования выполняет манглирование имён немного по-разному, поэтому для того чтобы функция Rust была доступна по имени для других языков, мы должны отключить манглирование имён компилятором Rust. Это небезопасно, потому что могут возникать коллизии имён между библиотеками без встроенного манглирования, поэтому наша ответственность убедиться, что выбранное нами имя безопасно для экспорта без манглирования.
В следующем примере мы делаем функцию call_from_c доступной из кода на C после её компиляции в общую библиотеку и связывания из 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. Если два потока обращаются к одной и той же изменяемой глобальной переменной, это может вызвать гонку данных.
В Rust глобальные переменные называются статическими переменными. Листинг 20-10 показывает пример объявления и использования статической переменной со строковым срезом в качестве значения.
static HELLO_WORLD: &str = "Hello, world!"; fn main() { println!("name is: {HELLO_WORLD}"); }
Статические переменные похожи на константы, которые мы обсудили в разделе «Константы» главы 3. Имена статических переменных по соглашению записываются в SCREAMING_SNAKE_CASE. Статические переменные могут хранить только ссылки со временем жизни 'static, что означает, что компилятор Rust может определить время жизни, и нам не требуется явно его аннотировать. Обращение к неизменяемой статической переменной безопасно.
Тонкое различие между константами и неизменяемыми статическими переменными в том, что значения в статической переменной имеют фиксированный адрес в памяти. Использование значения всегда будет обращаться к одним и тем же данным. Константы, с другой стороны, могут дублировать свои данные каждый раз при использовании. Другое различие в том, что статические переменные могут быть изменяемыми. Обращение к изменяемым статическим переменным и их изменение небезопасно. Листинг 20-11 показывает, как объявить, обратиться и изменить изменяемую статическую переменную с именем COUNTER.
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)); } }
Как и с обычными переменными, мы указываем изменяемость с помощью ключевого слова mut. Любой код, который читает или записывает в COUNTER, должен находиться внутри блока unsafe. Этот код компилируется и печатает COUNTER: 3, как мы ожидаем, потому что он однопоточный. Наличие нескольких потоков, обращающихся к COUNTER, скорее всего, приведёт к гонкам данных, поэтому это неопределённое поведение. Следовательно, нам нужно пометить всю функцию как unsafe и документировать ограничение безопасности, чтобы любой, вызывающий функцию, знал, что они могут и не могут безопасно делать.
Всякий раз, когда мы пишем небезопасную функцию, идиоматично написать комментарий, начинающийся с SAFETY, и объяснить, что вызывающий должен сделать, чтобы безопасно вызвать функцию. Аналогично, всякий раз, когда мы выполняем небезопасную операцию, идиоматично написать комментарий, начинающийся с SAFETY, чтобы объяснить, как соблюдаются правила безопасности.
Кроме того, компилятор не позволит вам создавать ссылки на изменяемую статическую переменную. Вы можете обращаться к ней только через сырой указатель, созданный одним из операторов сырого заимствования. Это включает случаи, когда ссылка создаётся неявно, как при использовании в println! в этом листинге кода. Требование, чтобы ссылки на изменяемые статические переменные могли создаваться только через сырые указатели, помогает сделать требования безопасности для их использования более очевидными.
С изменяемыми данными, доступными глобально, трудно убедиться, что нет гонок данных, поэтому Rust считает изменяемые статические переменные небезопасными. Где возможно, предпочтительнее использовать техники конкурентности и потокобезопасные умные указатели, которые мы обсуждали в главе 16, чтобы компилятор проверял, что доступ к данным из разных потоков выполняется безопасно.
Реализация небезопасного типажа
Мы можем использовать unsafe для реализации небезопасного типажа. Типаж небезопасен, когда хотя бы один из его методов имеет некоторое инвариантное условие, которое компилятор не может проверить. Мы объявляем, что типаж является unsafe, добавляя ключевое слово unsafe перед trait и помечая реализацию типажа также как unsafe, как показано в Листинге 20-12.
unsafe trait Foo { // methods go here } unsafe impl Foo for i32 { // method implementations go here } fn main() {}
Используя unsafe impl, мы обещаем, что будем соблюдать инварианты, которые компилятор не может проверить.
В качестве примера вспомните маркерные типажи Sync и Send, которые мы обсуждали в разделе «Расширяемая конкурентность с типажами Sync и Send» главы 16: компилятор реализует эти типажи автоматически, если наши типы состоят целиком из других типов, которые реализуют Send и Sync. Если мы реализуем тип, содержащий тип, который не реализует Send или Sync, такой как сырые указатели, и хотим пометить этот тип как Send или Sync, мы должны использовать unsafe. Rust не может проверить, что наш тип соблюдает гарантии, что его можно безопасно отправлять между потоками или к нему можно обращаться из нескольких потоков; следовательно, нам нужно выполнять эти проверки вручную и указывать на это с помощью unsafe.
Обращение к полям union
Последнее действие, которое работает только с unsafe, — это обращение к полям union. Union похож на struct, но только одно объявленное поле используется в конкретном экземпляре одновременно. Union в основном используются для взаимодействия с union в коде на C. Обращение к полям union небезопасно, потому что Rust не может гарантировать тип данных, который в данный момент хранится в экземпляре union. Вы можете узнать больше о union в справочнике Rust.
Использование Miri для проверки небезопасного кода
При написании небезопасного кода вы можете захотеть проверить, что написанное вами действительно безопасно и корректно. Один из лучших способов сделать это — использовать Miri, официальный инструмент Rust для обнаружения неопределённого поведения. В то время как проверка заимствований — это статический инструмент, работающий на этапе компиляции, 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-11.
$ 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`
COUNTER: 3
Miri правильно предупреждает нас, что у нас есть разделяемые ссылки на изменяемые данные. Здесь Miri выдаёт только предупреждение, потому что это не гарантированно неопределённое поведение в этом случае, и он не говорит нам, как исправить проблему. Но по крайней мере мы знаем, что есть риск неопределённого поведения, и можем подумать о том, как сделать код безопасным. В некоторых случаях Miri также может обнаружить явные ошибки — шаблоны кода, которые точно неверны, — и давать рекомендации о том, как исправить эти ошибки.
Miri не ловит всё, что вы можете сделать не так при написании небезопасного кода. Miri — это инструмент динамического анализа, поэтому он обнаруживает только проблемы с кодом, который фактически выполняется. Это означает, что вам нужно будет использовать его вместе с хорошими техниками тестирования, чтобы повысить уверенность в небезопасном коде, который вы написали. Miri также не охватывает все возможные способы, которыми ваш код может быть некорректным.
Иначе говоря: если Miri обнаруживает проблему, вы знаете, что есть ошибка, но просто потому, что Miri не обнаруживает ошибку, это не значит, что проблемы нет. Однако он может обнаружить многое. Попробуйте запустить его на других примерах небезопасного кода в этой главе и посмотрите, что он говорит!
Вы можете узнать больше о Miri на его репозитории GitHub.
Когда использовать небезопасный код
Использование unsafe для использования одной из пяти сверхспособностей, только что обсуждённых, не является неправильным или даже осуждаемым, но получить небезопасный код корректным сложнее, потому что компилятор не может помочь обеспечить безопасность памяти. Когда у вас есть причина использовать небезопасный код, вы можете это сделать, и наличие явной аннотации unsafe облегчает отслеживание источника проблем, когда они возникают. Всякий раз, когда вы пишете небезопасный код, вы можете использовать Miri, чтобы помочь вам быть более уверенным в том, что написанный вами код соблюдает правила Rust.
Для гораздо более глубокого исследования того, как эффективно работать с небезопасным Rust, прочитайте официальное руководство Rust по этой теме, Rustonomicon.