Небезпечний (unsafe) Rust
Увесь код, про який ми досі говорили, мав гарантії безпеки пам’яті Rust, що забезпечувалися під час компіляції. Однак у Rust є друга мова, прихована всередині нього, яка не забезпечує цих гарантій безпеки пам’яті: вона називається unsafe Rust і працює так само, як звичайний Rust, але дає нам додаткові суперздібності.
Unsafe Rust існує тому, що за своєю природою статичний аналіз є консервативним. Коли компілятор намагається визначити, чи дотримується код гарантій, для нього краще відхилити деякі коректні програми, ніж прийняти деякі некоректні програми. Хоча код може бути правильним, якщо компілятор Rust не має достатньо інформації, щоб бути впевненим, він відхилить код. У таких випадках ви можете використати небезпечний код, щоб сказати компілятору: «Повір мені, я знаю, що роблю». Але майте на увазі, що ви використовуєте unsafe Rust на власний ризик: якщо ви використовуєте небезпечний код неправильно, можуть виникнути проблеми через небезпеку пам’яті, такі як розіменування нульового вказівника.
Ще одна причина, чому Rust має небезпечне альтер его, полягає в тому, що апаратне забезпечення комп’ютера, на якому він працює, за своєю суттю є небезпечним. Якби Rust не дозволяв вам виконувати небезпечні операції, ви не змогли б робити певні завдання. Rust має дозволяти вам виконувати низькорівневе системне програмування, наприклад безпосередньо взаємодіяти з операційною системою або навіть писати власну операційну систему. Робота з низькорівневим системним програмуванням — одна з цілей мови. Давайте дослідимо, що ми можемо робити з unsafe Rust і як це робити.
Виконання небезпечних суперздібностей
Щоб перейти до unsafe Rust, використайте ключове слово unsafe, а потім
почніть новий блок, який містить небезпечний код. У unsafe Rust ви можете
виконати п’ять дій, яких не можна виконати в безпечному Rust, і які ми
називаємо unsafe superpowers. Ці суперздібності включають можливість:
- Розіменувати raw pointer.
- Викликати небезпечну функцію або метод.
- Отримувати доступ до змінної static mutable або змінювати її.
- Реалізувати unsafe trait.
- Отримувати доступ до полів
unions.
Важливо розуміти, що unsafe не вимикає перевірник запозичень і не вимикає
жодну з інших перевірок безпеки Rust: якщо ви використовуєте посилання в
небезпечному коді, воно все одно буде перевірятися. Ключове слово unsafe
лише дає вам доступ до цих п’яти можливостей, які потім не перевіряються
компілятором на безпеку пам’яті. Усередині unsafe block ви все одно отримаєте
певний рівень безпеки.
Крім того, unsafe не означає, що код усередині блоку обов’язково є
небезпечним або що він точно матиме проблеми з безпекою пам’яті: задум у тому,
що ви, як програміст, забезпечите, щоб код усередині unsafe block отримував
доступ до пам’яті коректним способом.
Люди помиляються, і помилки траплятимуться, але завдяки вимозі, щоб ці п’ять
небезпечних операцій були всередині блоків, позначених unsafe, ви знатимете,
що будь-які помилки, пов’язані з безпекою пам’яті, мають бути всередині
unsafe block. Тримайте unsafe blocks маленькими; ви будете вдячні собі
пізніше, коли досліджуватимете помилки пам’яті.
Щоб максимально ізолювати небезпечний код, найкраще поміщати такий код у
безпечну абстракцію та надавати безпечний API, про що ми поговоримо пізніше в
розділі, коли розглядатимемо небезпечні функції та методи. Частини standard
library реалізовані як безпечні абстракції над небезпечним кодом, який було
аудитовано. Обгортання небезпечного коду в безпечну абстракцію запобігає
«витіканню» використання unsafe у всі ті місця, де ви або ваші користувачі
можуть захотіти використовувати функціональність, реалізовану за допомогою
unsafe code, тому що використання безпечної абстракції є безпечним.
Давайте по черзі розглянемо кожну з п’яти unsafe superpowers. Також подивімося на деякі абстракції, які надають безпечний інтерфейс до небезпечного коду.
Розіменування raw pointer
У главі 4, у розділі “Dangling References”, ми згадували, що компілятор гарантує, що посилання завжди є дійсними.
Unsafe Rust має два нові типи, які називаються raw pointers, що схожі на
посилання. Як і посилання, raw pointers можуть бути незмінними або змінними й
записуються як *const T і *mut T, відповідно. Зірочка — це не оператор
розіменування; вона є частиною назви типу. У контексті raw pointers
незмінний означає, що до вказівника не можна безпосередньо присвоїти значення
після його розіменування.
На відміну від посилань і smart pointers, raw pointers:
- Можуть ігнорувати правила запозичення, маючи як незмінні, так і змінні вказівники або кілька змінних вказівників на те саме місце
- Не гарантується, що вказують на дійсну пам’ять
- Можуть бути null
- Не реалізують жодного автоматичного очищення
Відмовляючись від того, щоб Rust забезпечував ці гарантії, ви можете відмовитися від гарантованої безпеки в обмін на вищу продуктивність або можливість взаємодіяти з іншою мовою чи апаратним забезпеченням, де гарантії Rust не застосовуються.
Listing 20-1 показує, як створити незмінний і змінний raw pointer.
fn main() {
let mut num = 5;
let r1 = &raw const num;
let r2 = &raw mut num;
}
Зверніть увагу, що ми не включаємо ключове слово unsafe у цей код. Ми можемо
створювати raw pointers у безпечному коді; ми просто не можемо розіменовувати
raw pointers поза unsafe block, як ви побачите за мить.
Ми створили raw pointers, використавши raw borrow operators: &raw const num
створює *const i32 незмінний raw pointer, а &raw mut num створює *mut i32 змінний raw pointer. Оскільки ми створили їх безпосередньо з локальної
змінної, ми знаємо, що ці конкретні raw pointers є дійсними, але ми не можемо
робити таке припущення про будь-який raw pointer.
Щоб це продемонструвати, далі ми створимо raw pointer, щодо дійсності якого не
можна бути настільки впевненими, використавши ключове слово as, щоб
привести значення, замість використання raw borrow operator. Listing 20-2
показує, як створити raw pointer до довільного місця в пам’яті. Спроба
використовувати довільну пам’ять є невизначеною: там можуть бути дані за цією
адресою, а можуть і не бути, компілятор може оптимізувати код так, що доступу
до пам’яті не буде, або програма може завершитися з segmentation fault. Зазвичай
немає вагомої причини писати такий код, особливо в тих випадках, коли замість
цього можна використати raw borrow operator, але це можливо.
fn main() {
let address = 0x012345usize;
let r = address as *const i32;
}
Пригадайте, що ми можемо створювати raw pointers у безпечному коді, але не
можемо розіменовувати raw pointers і читати дані, на які вони вказують. У
Listing 20-3 ми використовуємо оператор розіменування * для raw pointer, що
вимагає unsafe block.
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);
}
}
Створення вказівника не завдає шкоди; проблеми виникають лише тоді, коли ми намагаємося отримати доступ до значення, на яке він вказує, і тоді можемо зіткнутися з недійсним значенням.
Також зауважте, що в Listings 20-1 і 20-3 ми створили raw pointers *const i32
і *mut i32, які обидва вказували на те саме місце в пам’яті, де зберігається
num. Якщо б ми замість цього спробували створити незмінне і змінне посилання
на num, код не скомпілювався б, тому що правила власності Rust не дозволяють
змінне посилання одночасно з будь-якими незмінними посиланнями. З raw pointers
ми можемо створити змінний вказівник і незмінний вказівник на те саме місце та
змінювати дані через змінний вказівник, потенційно створюючи стан гонки даних.
Будьте обережні!
За наявності всіх цих небезпек, навіщо взагалі використовувати raw pointers? Один із основних випадків використання — це взаємодія з кодом C, як ви побачите в наступному розділі. Інший випадок — побудова безпечних абстракцій, яких не розуміє перевірник запозичень. Ми представимо небезпечні функції, а потім розглянемо приклад безпечної абстракції, яка використовує небезпечний код.
Виклик небезпечної функції або методу
Другий тип операції, яку ви можете виконати в unsafe block, — це виклик
небезпечних функцій. Небезпечні функції та методи виглядають точно так само, як
звичайні функції та методи, але перед рештою визначення мають додаткове
unsafe. Ключове слово unsafe в цьому контексті вказує на те, що функція має
вимоги, яких ми повинні дотриматися під час її виклику, тому що Rust не може
гарантувати, що ми виконали ці вимоги. Викликаючи небезпечну функцію всередині
unsafe block, ми говоримо, що прочитали документацію цієї функції й беремо на
себе відповідальність за дотримання контрактів функції.
Ось небезпечна функція на ім’я dangerous, яка нічого не робить у своєму
тілі:
fn main() {
unsafe fn dangerous() {}
unsafe {
dangerous();
}
}
Ми повинні викликати функцію dangerous всередині окремого unsafe block.
Якщо ми спробуємо викликати dangerous без unsafe block, отримаємо помилку:
$ 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 block ми стверджуємо перед Rust, що прочитали документацію функції,
розуміємо, як правильно її використовувати, і перевірили, що виконуємо
контракт функції.
Щоб виконувати небезпечні операції в тілі unsafe function, вам усе одно
потрібно використовувати unsafe block, так само як і в звичайній функції, і
компілятор попередить вас, якщо ви забудете. Це допомагає нам тримати
unsafe blocks якомога меншими, оскільки небезпечні операції можуть бути не
потрібні в усьому тілі функції.
Створення безпечної абстракції над небезпечним кодом
Лише тому, що функція містить небезпечний код, не означає, що нам потрібно
позначати всю функцію як unsafe. Насправді обгортання небезпечного коду в
безпечну функцію — це поширена абстракція. Як приклад, давайте вивчимо функцію
split_at_mut зі standard library, яка потребує деякого небезпечного коду. Ми
дослідимо, як могли б її реалізувати. Цей безпечний метод визначено для
mutable slices: він бере один slice і робить із нього два, розділяючи slice за
індексом, переданим як аргумент. Listing 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]);
}
Ми не можемо реалізувати цю функцію, використовуючи лише безпечний Rust.
Спроба може виглядати приблизно як у Listing 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);
}
Ця функція спочатку отримує загальну довжину slice. Потім вона стверджує, що індекс, переданий як параметр, знаходиться в межах slice, перевіряючи, чи він менший або дорівнює довжині. Це твердження означає, що якщо ми передамо індекс, який більший за довжину, щоб розділити slice в цій точці, функція панікує, перш ніж спробує використати цей індекс.
Потім ми повертаємо два mutable slices у кортежі: один від початку
початкового slice до індексу mid і ще один від mid до кінця slice.
Коли ми спробуємо скомпілювати код у Listing 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 не може зрозуміти, що ми запозичуємо різні частини slice; він лише знає, що ми двічі запозичаємо з одного й того самого slice. Запозичення різних частин slice є принципово коректним, тому що два slices не перетинаються, але Rust недостатньо розумний, щоб це знати. Коли ми знаємо, що код коректний, а Rust — ні, настав час звернутися до небезпечного коду.
Listing 20-6 показує, як використати unsafe block, raw pointer і деякі
виклики небезпечних функцій, щоб реалізація 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);
}
Пригадайте з розділу “The Slice Type” у главі
4, що slice — це вказівник на деякі дані та довжина slice. Ми використовуємо
метод len, щоб отримати довжину slice, і метод as_mut_ptr, щоб отримати
доступ до raw pointer slice. У цьому випадку, оскільки ми маємо mutable slice
до значень i32, as_mut_ptr повертає raw pointer типу *mut i32, який ми
зберегли у змінній ptr.
Ми залишаємо твердження про те, що індекс mid знаходиться в межах slice.
Потім доходить черга до небезпечного коду: функція slice::from_raw_parts_mut
приймає raw pointer і довжину та створює slice. Ми використовуємо цю функцію,
щоб створити slice, який починається з ptr і має довжину mid елементів.
Потім ми викликаємо метод add на ptr, передаючи mid як аргумент, щоб
отримати raw pointer, який починається з mid, і створюємо slice, використовуючи
цей вказівник і решту кількості елементів після mid як довжину.
Функція slice::from_raw_parts_mut є небезпечною, тому що вона приймає raw
pointer і повинна довіряти, що цей вказівник є дійсним. Метод add для raw
pointers також є небезпечним, тому що він повинен довіряти, що місце зі зсувом
також є дійсним вказівником. Тому нам довелося помістити unsafe block навколо
викликів slice::from_raw_parts_mut і add, щоб ми могли їх викликати.
Дивлячись на код і додаючи твердження, що mid має бути меншим або дорівнювати
len, ми можемо сказати, що всі raw pointers, використані всередині unsafe
block, будуть дійсними вказівниками на дані всередині slice. Це прийнятне й
доречне використання unsafe.
Зверніть увагу, що нам не потрібно позначати отриману функцію split_at_mut
як unsafe, і ми можемо викликати цю функцію з безпечного Rust. Ми створили
безпечну абстракцію над небезпечним кодом з реалізацією функції, яка безпечно
використовує unsafe code, тому що вона створює лише дійсні вказівники з даних,
до яких ця функція має доступ.
Натомість використання slice::from_raw_parts_mut у Listing 20-7, імовірно,
завершиться аварійно, коли буде використано slice. Цей код бере довільне місце
в пам’яті та створює slice довжиною 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) };
}
Ми не володіємо пам’яттю в цьому довільному місці, і немає гарантії, що slice,
який створює цей код, містить дійсні значення i32. Спроба використати
values, ніби це дійсний slice, призводить до невизначеної поведінки.
Використання extern функцій для виклику зовнішнього коду
Іноді вашому коду Rust може знадобитися взаємодія з кодом, написаним іншою
мовою. Для цього Rust має ключове слово extern, яке полегшує створення та
використання Foreign Function Interface (FFI), тобто способу, яким мова
програмування може визначати функції та дозволяти іншій (foreign) мові
програмування викликати ці функції.
Listing 20-8 демонструє, як налаштувати інтеграцію з функцією abs зі
standard library C. Функції, оголошені всередині extern block, загалом є
небезпечними для виклику з коду Rust, тому extern blocks також мають бути
позначені unsafe. Причина в тому, що інші мови не забезпечують правил і
гарантій Rust, а Rust не може їх перевірити, тож відповідальність лягає на
програміста — забезпечити безпеку.
unsafe extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
Усередині unsafe extern "C" block ми перелічуємо назви та сигнатури зовнішніх
функцій з іншої мови, які хочемо викликати. Частина "C" визначає, який
application binary interface (ABI) використовує зовнішня функція: ABI
визначає, як викликати функцію на рівні асемблера. ABI "C" є найпоширенішим
і відповідає ABI мови програмування C. Інформація про всі ABI, які підтримує
Rust, доступна в the Rust Reference.
Кожен елемент, оголошений всередині unsafe extern block, неявно є
небезпечним. Однак деякі FFI functions є безпечними для виклику. Наприклад,
функція abs зі standard library C не має жодних міркувань щодо безпеки
пам’яті, і ми знаємо, що її можна викликати з будь-яким i32. У таких
випадках ми можемо використати ключове слово safe, щоб сказати, що ця
конкретна функція є безпечною для виклику, навіть попри те, що вона
знаходиться в unsafe extern block. Щойно ми вносимо цю зміну, її виклик
більше не потребує unsafe block, як показано в Listing 20-9.
unsafe extern "C" {
safe fn abs(input: i32) -> i32;
}
fn main() {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
Позначення функції як safe не робить її автоматично безпечною! Натомість це
подібно до обіцянки, яку ви даєте Rust, що вона є безпечною. На вас усе ще
лежить відповідальність переконатися, що ця обіцянка виконується!
Виклик функцій Rust з інших мов
Ми також можемо використовувати extern, щоб створити інтерфейс, який
дозволяє іншим мовам викликати функції Rust. Замість створення цілого extern
block ми додаємо ключове слово extern і вказуємо ABI, який слід
використовувати, прямо перед ключовим словом fn для відповідної функції.
Нам також потрібно додати анотацію #[unsafe(no_mangle)], щоб сказати
компілятору Rust не змінювати ім’я цієї функції. Mangling — це коли
компілятор змінює назву, яку ми дали функції, на іншу назву, яка містить більше
інформації для використання іншими частинами процесу компіляції, але є менш
зрозумілою для людини. Компілятор кожної мови програмування трохи по-різному
mangles імена, тому для того, щоб функцію Rust могли називати інші мови, ми
маємо вимкнути mangling імен компілятором Rust. Це небезпечно, тому що без
вбудованого mangling можуть виникати конфлікти імен між бібліотеками, тож на
нас лежить відповідальність переконатися, що вибране ім’я безпечно
експортувати без mangling.
У наступному прикладі ми робимо функцію call_from_c доступною з коду C після
того, як її буде скомпільовано в shared library і під’єднано з C:
#[unsafe(no_mangle)]
pub extern "C" fn call_from_c() {
println!("Just called a Rust function from C!");
}
Таке використання extern вимагає unsafe лише в атрибуті, а не в extern
block.
Отримання доступу до змінної static mutable або зміна її
У цій книзі ми ще не говорили про глобальні змінні, які Rust підтримує, але які можуть бути проблематичними через правила власності Rust. Якщо два потоки отримують доступ до однієї й тієї самої змінної mutable global variable, це може спричинити стан гонки даних.
У Rust глобальні змінні називаються static variables. Listing 20-10 показує приклад оголошення та використання static variable зі string slice як значенням.
static HELLO_WORLD: &str = "Hello, world!";
fn main() {
println!("value is: {HELLO_WORLD}");
}
Static variables схожі на constants, про які ми говорили в розділі
“Declaring Constants” у главі 3. Назви static
variables за домовленістю записують у SCREAMING_SNAKE_CASE. Static variables
можуть зберігати лише посилання з часом життя 'static, що означає, що
компілятор Rust може визначити час життя, і нам не потрібно позначати його
явно. Отримувати доступ до незмінної static variable безпечно.
Непомітна різниця між constants і незмінними static variables полягає в тому,
що значення в static variable мають фіксовану адресу в пам’яті. Використання
значення завжди отримуватиме доступ до тих самих даних. Constants, з іншого
боку, можуть дублювати свої дані щоразу, коли їх використовують. Ще одна
відмінність полягає в тому, що static variables можуть бути mutable. Отримання
доступу до mutable static variables і їх зміна є unsafe. Listing 20-11
показує, як оголосити, отримати доступ і змінити mutable static variable на
ім’я 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 block. Код у Listing 20-11 компілюється і друкує COUNTER: 3, як ми
й очікували, тому що він однопотоковий. Якщо кілька потоків отримуватимуть
доступ до COUNTER, це, ймовірно, призведе до станів гонки даних, тому це є
невизначеною поведінкою. Отже, нам потрібно позначити всю функцію як unsafe
і задокументувати обмеження безпеки, щоб кожен, хто викликає цю функцію, знав,
що йому дозволено, а що не дозволено робити безпечно.
Щоразу, коли ми пишемо небезпечну функцію, ідіоматично писати коментар, що
починається з SAFETY, і пояснювати, що саме викликач має зробити, щоб
викликати функцію безпечно. Так само щоразу, коли ми виконуємо небезпечну
операцію, ідіоматично писати коментар, що починається з SAFETY, щоб
пояснити, як дотримуються правил безпеки.
Крім того, компілятор за замовчуванням відхилить будь-яку спробу створити
посилання на змінну mutable static variable через compiler lint. Ви повинні або
явно відмовитися від захисту цього lint, додавши анотацію
#[allow(static_mut_refs)], або отримувати доступ до mutable static variable
через raw pointer, створений одним із raw borrow operators. Це стосується й
випадків, коли посилання створюється неявно, як, наприклад, коли воно
використовується в println! у цьому фрагменті коду. Вимога створювати
посилання на mutable static variables через raw pointers допомагає зробити
вимоги безпеки для їх використання очевиднішими.
Коли mutable data є доступними глобально, важко гарантувати, що не буде станів гонки даних, саме тому Rust вважає mutable static variables небезпечними. За можливості краще використовувати техніки конкурентності та thread-safe smart pointers, які ми обговорювали в главі 16, щоб компілятор перевіряв, що доступ до даних із різних потоків відбувається безпечно.
Реалізація unsafe trait
Ми можемо використовувати unsafe, щоб реалізувати unsafe trait. Трейт є
unsafe, коли принаймні один із його методів має інваріант, який компілятор не
може перевірити. Ми оголошуємо, що трейт є unsafe, додаючи ключове слово
unsafe перед trait, і також позначаємо реалізацію трейтa як unsafe, як
показано в Listing 20-12.
unsafe trait Foo {
// methods go here
}
unsafe impl Foo for i32 {
// method implementations go here
}
fn main() {}
Використовуючи unsafe impl, ми обіцяємо, що дотримаємося інваріантів, які
компілятор не може перевірити.
Як приклад, пригадайте marker traits Send і Sync, про які ми говорили в
розділі “Extensible Concurrency with Send and Sync” у главі 16: компілятор реалізує ці трейтa автоматично, якщо наші
типи складаються повністю з інших типів, які реалізують Send і Sync. Якщо
ми реалізуємо тип, що містить тип, який не реалізує Send або Sync, такий як
raw pointers, і хочемо позначити цей тип як Send або Sync, ми повинні
використати unsafe. Rust не може перевірити, що наш тип дотримується
гарантій, що його можна безпечно надсилати між потоками або отримувати до
нього доступ із кількох потоків; отже, нам потрібно зробити ці перевірки
вручну й позначити це за допомогою unsafe.
Отримання доступу до полів union
Остання дія, яка працює лише з unsafe, — це доступ до полів union. union
схожа на struct, але лише одне оголошене поле використовується в конкретному
екземплярі в один момент часу. Union переважно використовуються для
взаємодії з union у коді C. Доступ до полів union є небезпечним, тому що Rust
не може гарантувати тип даних, який зараз зберігається в екземплярі union. Ви
можете дізнатися більше про union в the Rust Reference.
Використання Miri для перевірки небезпечного коду
Під час написання небезпечного коду ви, можливо, захочете перевірити, що написане вами дійсно є безпечним і коректним. Один із найкращих способів зробити це — використати Miri, офіційний інструмент Rust для виявлення невизначеної поведінки. Якщо перевірник запозичень — це static інструмент, який працює під час компіляції, то Miri — це dynamic інструмент, який працює під час виконання. Він перевіряє ваш код, запускаючи вашу програму або її test suite, і виявляє, коли ви порушуєте правила, які він розуміє щодо того, як Rust має працювати.
Використання Miri потребує nightly build Rust (про що ми говоримо докладніше в
Appendix G: How Rust is Made and “Nightly Rust”). Ви
можете встановити і nightly версію Rust, і інструмент Miri, ввівши rustup +nightly component add miri. Це не змінює, яку версію Rust використовує ваш
проєкт; це лише додає інструмент у вашу систему, щоб ви могли використовувати
його, коли захочете. Ви можете запустити Miri для проєкту, ввівши cargo +nightly miri run або cargo +nightly miri test.
Як приклад того, наскільки це може бути корисно, розгляньте, що станеться, коли ми запустимо його проти Listing 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 повертає помилку там, де в Listing 20-7 є невизначена поведінка, тому що ми маємо dangling pointer. Завдяки Miri ми тепер знаємо, що існує ризик невизначеної поведінки, і можемо подумати, як зробити код безпечним. У деяких випадках Miri навіть може надати рекомендації щодо виправлення помилок.
Miri не виявляє всього, що ви можете зробити неправильно під час написання небезпечного коду. Miri — це інструмент динамічного аналізу, тож він виявляє лише проблеми з кодом, який фактично виконується. Це означає, що вам потрібно використовувати його разом із хорошими методами тестування, щоб підвищити впевненість у написаному небезпечному коді. Miri також не охоплює кожен можливий спосіб, у який ваш код може бути unsound.
Іншими словами: якщо Miri дійсно знаходить проблему, ви знаєте, що є помилка, але лише тому, що Miri не знаходить помилку, не означає, що проблеми немає. Проте він може виявити дуже багато. Спробуйте запустити його на інших прикладах небезпечного коду в цій главі й подивіться, що він скаже!
Ви можете дізнатися більше про Miri в його репозиторії GitHub.
Коректне використання небезпечного коду
Використання unsafe, щоб скористатися однією з п’яти щойно обговорених
суперздібностей, не є неправильним і навіть не викликає несхвалення, але
зробити unsafe code коректним складніше, тому що компілятор не може
допомогти забезпечити безпеку пам’яті. Коли у вас є причина використовувати
unsafe code, ви можете це робити, а явна анотація unsafe полегшує
встановлення джерела проблем, коли вони виникають. Щоразу, коли ви пишете
небезпечний код, ви можете використовувати Miri, щоб бути більш впевненими,
що написаний вами код дотримується правил Rust.
Щоб значно глибше дослідити, як ефективно працювати з unsafe Rust, прочитайте
офіційний посібник Rust для unsafe, The Rustonomicon.