Небезпечний Rust

Весь код, що ми до цього моменту обговорювали, мав гарантії безпеки памʼяті, які Rust забезпечував під час компіляції. Однак, Rust має другу мову мову, сховану всередині нього, що не надає ці гарантії: вона називається небезпечний Rust і працює так само як і звичайний Rust, але надає додаткові суперсили.

Небезпечний Rust існує тому, що за своєю природою, статичний аналіз є консервативним. Коли компілятор намається визначити чи надає код потрібні гарантії, краще відхилити деякі валідні програми, ніж в подальшому скомпілювати невалідні програми. Хоча код може бути в порядку, якщо компілятор Rust не має достатньо інформації, щоб бути в цьому впевненим, він відхилить такий код. В таких випадках ви можете використовувати небезпечний код, щоб сказати компілятору, "Довірся мені, я знаю що роблю". Однак майте на увазі, що ви використовуєте небезпечний Rust на свій страх і ризик: якщо ви неправильно використовуєте небезпечний код, можуть виникнути проблеми, повʼязані з памʼяттю, такі як розіменування нульового вказівника (null pointer).

Іншою причиною, чому Rust має небезпечне альтер его, є те, що саме комп'ютерне обладнання є небезпечним за своєю суттю. Якби Rust не дозволяв вам виконувати небезпечні операції, ви не могли б виконувати певні завдання. Rust вимушений дозволити вам низькорівневе системне програмування, таке як пряму взаємодію з операційною системою чи навіть написання власної операційної системи. Робота з низькорівневим системним програмуванням є однією з цілей мови. Дослідимо, що ми можемо зробити з небезпечним Rust і як це зробити.

Небезпечні суперсили

Щоб перейти до небезпечного Rust, скористайтеся ключовим словом unsafe і почніть новий блок, що містить небезпечний код. Ви можете робити п'ять дій в небезпечному Rust, які не можна робити в безпечному Rust, який ми називаємо небезпечними суперсилами. Ці суперсили охоплюють такі можливості:

  • Розіменування сирого вказівника
  • Виклик небезпечної функції або методу
  • Доступ або модифікація мутабельних статичних змінних
  • Реалізація небезпечного трейта
  • Доступ до полів union

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

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

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

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

Подивімося на кожну з п'яти небезпечних суперсил по черзі. Ми також подивимося на деякі абстракції, що надають безпечний інтерфейс для небезпечного коду.

Розіменування сирого вказівника

У Розділі 4, підрозділі "Підвішені посилання" , ми згадували, що компілятор гарантує, що посилання є завжди коректними. Небезпечний Rust має два нові типи під назвою

сирі вказівники, схожі на посилання. Як і з посиланнями, сирі вказівники можуть бути немутабельними або мутабельними і записуються як *const T і *mut T відповідно. Зірочка тут не є оператором розіменування; це частина назви типу. У контексті сирих вказівників, немутабельність означає, що вказівнику не можна присвоїти значення після розіменування.

На відміну від посилань і розумних вказівників, сирі вказівники:

  • Можуть ігнорувати правила позичання, маючи як немутабельні, так і мутабельні вказівники або декілька мутабельних вказівників на одне місце
  • Не гарантують, що вказують на коректну пам'ять
  • Можуть бути null
  • Не реалізовують жодного автоматичного очищення

Відмовляючись від цих гарантій Rust, ви поступаєтеся гарантованою безпекою в обмін на вищу продуктивність чи здатність взаємодії з іншою мовою чи обладнянням, які не забезпечують гарантій Rust.

Блок коду 19-1 показує, як створити немутабельний і мутабельний сирі вказівники з посилання.

fn main() {
    let mut num = 5;

    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;
}

Блок коду 19-1: Створення сирих вказівників із посилань

Зверніть увагу, що ми не включаємо в цей код ключове слово unsafe. Ми можемо створювати сирі вказівники у безпечному коді; ми не можемо лише розіменовувати сирі вказівники поза блоками unsafe, як ви зараз побачите.

Ми створили сирі вказівники за допомогою as, щоб перетворити немутабельне і мутабельне посилання у відповідні типи сирих вказівників. Оскільки ми створили їх безпосередньо з посилань, які є гарантовано коректними, ми знаємо, що ці конкретні сирі вказівники є коректними, але ми не можемо робити таке припущення про довільні сирі вказівники.

Щоб продемонструвати це, дали ми створимо сирий вказівник, у коректності якого ми не можемо бути певними. Блок коду 19-2 показує, як створити сирий вказівник до довільного місця у пам'яті. Спроба використання довільної пам'яті є невизначеною операцією: за вказаною адресою можуть бути дані або ні, компілятор може оптимізувати код, прибравши доступ до пам'яті, або програма може завершитися з помилкою сегментації. Зазвичай немає жодної причини писати подібний код, але це можливо.

fn main() {
    let address = 0x012345usize;
    let r = address as *const i32;
}

Блок коду 19-2: Створення сирого вказівника на довільну адресу памʼяті

Пригадайте, що ми можемо створювати сирі вказівники в безпечному коді, але ми не можемо розіменовувати сирі вказівники і читати дані, на які вони вказують. У Блоці коду 19-3 ми використовуємо оператор розіменування * на сирому вказівнику, що потребує блоку unsafe.

fn main() {
    let mut num = 5;

    let r1 = &num as *const i32;
    let r2 = &mut num as *mut i32;

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

Блок коду 19-3: Розіменування сирого вказівника в блоці unsafe

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

Зауважте, що у Блоках коду 19-1 і 19-3 ми створили сирі вказівники *const i32 і *mut i32, які обидва вказують на те саме місце в пам'яті, де зберігається num. Якби ми натомість спробували створити немутабельне і мутабельне посилання на num, код би не скомпілювався, бо правила володіння Rust забороняють мати мутабельне посилання одночасно з немутабельними посиланнями. З сирими вказівниками ми можемо створити мутабельний і немутабельний вказівники на одне й те саме місце і змінити дані через мутабельний вказівник, потенційно створивши гонитву даних. Будьте обережні!

З усіма цими небезпеками, нащо вам узагалі потрібні сирі вказівники? Одним з основних застосувань є взаємодія з кодом С, як ви побачите в наступному розділі, "Виклик небезпечної функції або Методу." Інший сценарій використання - побудова безпечної абстракції, яку borrow checker не розуміє. Ми представимо небезпечні функції, а потім подивимося на приклад безпечної абстракції, яка використовує небезпечний код.

Виклик небезпечної функції або методу

Другий тип операцій, які ви можете виконувати в блоці 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 is unsafe and requires unsafe function or 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` due to previous error

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

Тіла небезпечних функцій є фактично блоками unsafe, таким чином, щоб виконати інші небезпечні операції в небезпечній функції, нам не потрібно додавати ще один блок unsafe.

Створення безпечної абстракції над небезпечним кодом

Те, що функція містить небезпечний код, не означає, що нам потрібно позначити всю функцію як небезпечну. Насправді обгортання небезпечного коду в безпечну функцію є звичайною абстракцією. Як приклад, розглянемо функцію split_at_mut зі стандартної бібліотеки, якій потрібен небезпечний код для роботи. Ми дослідимо, як ми можемо її реалізувати. Цей безпечний метод визначено на мутабельних слайсах: він бере слайс і робить з нього два, ділячи слайс по індексу, заданому аргументом. Блок коду 19-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]);
}

Блок коду 19-4: Використання безпечної функції split_at_mut

Ми не можемо реалізувати цю функцію за допомогою лише безпечного Rust. Спроба може бути дещо схожою на Блок коду 19-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);
}

Блок коду 19-5: спроба реалізації split_at_mut за допомогою лише безпечного Rust

Функція спочатку отримує загальну довжину слайса. Потім стверджує, що індекс, заданий параметром, знаходиться в слайсі, перевіривши, що він менший чи дорівнює довжині. Твердження означає, що якщо ми передамо індекс, більший за довжину, щоб розділити по ньому слайс, функція запанікує перед спробою використати цей індекс.

Тоді ми повертаємо два мутабельні слайси у кортежі: один від початку вихідного слайса до індексу mid, і другий з mid до кінця слайса.

Коли ми спробуємо скомпілювати код в Блоці коду 19-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`

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

Перевірка позичання Rust не розуміє, що ми позичаємо різні частини слайса; вона лише знає, що ми позичаємо з одного слайса двічі. Позичання різних частин слайса є принципово правильним, оскільки два слайси не перетинаються, але Rust недостатньо розумна, щоб знати це. Коли ми знаємо, що код не містить помилок, але Rust так не вважає, настає час долучити небезпечний код.

Блок коду 19-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);
}

Блок коду 19-6: Використання небезпечного коду у реалізації функції 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. Ми створили безпечну абстракція для небезпечного коду з реалізацією функції, які використовує код unsafe у безпечний спосіб, тому що він створює тільки коректні вказівники з даних, до яких ця функція має доступ.

Натомість використання slice::from_raw_parts_mut у Блоці коду 19-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) };
}

Блок коду 19-7: створення слайса з довільного розташування в пам'яті

Ми не володіємо пам'яттю у цьому довільному місці, і немає гарантії, що слайс, створений цим кодом, містить коректні значення i32. Спроба використання values, ніби це коректний слайс, призводить до невизначеної поведінки.

Використання extern функцій для виклику зовнішнього коду

Іноді вашому коду Rust потрібно взаємодіяти з кодом, написаним іншою мовою. Для цього Rust має ключове слово extern, яке полегшує створення і використання Інтерфейсу Зовнішніх Функцій (Foreign Function Interface, FFI). FFI - це засіб мови програмування для визначення функцій і дозволу іншій (зовнішній) мові програмування викликати ці функції.

Блок коду 19-8 демонструє, як налаштувати інтеграцію із функцією abs зі стандартної бібліотеки C. Функції, проголошені в блоках extern, завжди є небезпечними для виклику з коду Rust. Причина в тому, що інші мови не забезпечують правила і гарантії Rust, і Rust не може перевірити їх, тож відповідальність за гарантування безпеки покладається на програміста.

Файл: src/main.rs

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

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

Блок коду 19-8: проголошення і виклик зовнішньої (extern) функції, написаної іншою мовою

У блоці extern "C", ми перелічуємо назви і сигнатури зовнішніх функцій з іншої мови, які ми хочемо викликати. Частина "C" визначає, який двійковий інтерфейс застосунку (application binary interface, ABI) використовується зовнішньою функцією: ABI визначає спосіб виклику функції на рівні асемблера. ABI "C" є найпоширенішим і відповідає ABI мови програмування C.

Виклик функцій Rust з інших мов

Ми також можемо скористатися extern, щоб створити інтерфейс, що дозволяє іншим мовам викликати функції Rust. Замість створення цілого блоку extern, додамо ключове слово extern і зазначимо ABI, який треба використовувати перед ключовим словом fn у відповідної функції. Нам також треба додати анотацію #[no_mangle], щоб сказати компілятору Rust не перетворювати назву цієї функції. Перетворення (mangling) - це коли компілятор змінює назву, яку ми дали функції, на іншу назву, яка містить більше інформації для інших частин процесу компіляції, але є менш зручною для людини. Кожен компілятор мови програмування дещо по-різному перетворює назви, тому для того, щоб функцію Rust можна було назвати в інших мовах, ми маємо відключити перетворення назв компілятором Rust.

У наступному прикладі ми робимо функцію call_from_c доступною з C після того, як вона буде скомпільована у спільну бібліотеку та злінкована з C:

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

Використання extern не вимагає використання unsafe.

Доступ або модифікація мутабельних статичних змінних

У цій книзі ми ще не говорили про глобальні змінні, які Rust підтримує, але які можуть створювати проблеми з правилами володіння Rust. Якщо два потоки отримують доступ до однієї мутабельної глобальної змінної, це може викликати гонитву даних.

У Rust глобальні змінні називаються статичними змінними. Блок коду 19-9 показує приклад визначення і використання статичної змінної зі значенням стрічкового слайсу.

Файл: src/main.rs

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

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

Блок коду 19-9: визначення і використання немутабельної статичної змінної

Статичні змінні подібні до констант, які ми обговорювали в підрозділі "Константи" у Розділі 3. Назви статичних змінних за домовленістю пишуться ВЕРХНІМ_РЕГІСТРОМ_З_ПІДКРЕСЛЕННЯМИ. Статичні змінні можуть зберігати лише посилання з часом існування 'static, що означає, що компілятор Rust може знайти час існування, а ми не зобов'язані анотувати його явно. Доступ до немутабельних статичних змінних є безпечним.

Тонка різниця між константами і немутабельними статичними змінними полягає в тому, що значення в статичній змінній має фіксовану адресу в пам'яті. Коли ви використовуєте значення, то завжди матимете доступ до тих самих даних. Константи, з іншого боку, можуть дублювати дані всюди, де їх використовують. Інша відмінність полягає в тому, що статичні змінні можуть бути мутабельними. Доступ і зміна мутабельних статичних змінних є небезпечним. Блок коду 19-10 показує, як проголошувати, отримувати доступ і змінювати мутабельну статичну змінну, що називається COUNTER.

Файл: src/main.rs

static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);

    unsafe {
        println!("COUNTER: {}", COUNTER);
    }
}

Блок коду 19-10: читання і запис мутабельної статичної змінної є небезпечним

Як і зі звичайними змінними, ми визначаємо мутабельність ключовим словом mut. Будь-який код, який читає чи записує COUNTER, має бути в блоці unsafe. Цей код компілюється і виводить COUNTER: 3, як ми й маємо очікувати, бо він однопоточний. Якщо ж багато потоків матимуть доступ до COUNTER, це, швидше за все, призведе до гонитви даних.

З глобально доступними мутабельними даними важко забезпечити, щоб не було гонитви даних, і саме тому Rust вважає мутабельні статичні змінні небезпечними. Де це можливо, бажано використовувати методи конкурентності та потокобезпечні розумні вказівники, які ми обговорювали в Розділі 16, щоб компілятор перевіряв, що доступ до даних з різних потоків здійснюється безпечно.

Реалізація небезпечного трейта

Ми можемо скористатися unsafe для реалізації небезпечного трейта. Трейт є небезпечним, якщо хоча б один з його методів має якийсь інваріант, який компілятор не може перевірити. Ми проголошуємо, що трейт є небезпечним, додаючи ключове слово unsafe перед trait та позначивши реалізацію трейта як unsafe, як показано у Блоці коду 19-11.

unsafe trait Foo {
    // methods go here
}

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

fn main() {}

Блок коду 19-11: визначення та реалізація небезпечного трейта

За допомогою unsafe impl, ми обіцяємо, що дотримуватимемося інваріантів, які компілятор не може перевірити.

Як приклад, згадайте маркерні трейти Sync і Send, які ми обговорювали в підрозділі "Розширювана конкурентність із трейтами Sync і Send" у Розділі 16: компілятор реалізує ці трейти автоматично, якщо наші типи повністю складаються з типів Send і Sync. Якщо ми реалізуємо тип, який містить тип, що неє Send або Sync, такий як сирі вказівники, і ми хочемо позначити цей тип як Send або Sync, ми маємо використовувати unsafe. Довіра не може переконатися, що наш тип дотримується гарантій, щоб його можна було безпечно передавати між потоками або мати до нього доступ з декількох потоків; таким чином, нам потрібно робити ці перевірки вручну і позначити це за допомогою unsafe.

Доступ до полів обʼєднання

Остання дія, яка працює лише за допомогою unsafe - це доступ до полів об'єднання. Об'єднання (union) схоже на структуру struct, але лише одне проголошене поле використовується у конкретному екземплярі у кожен певний момент часу. Об'єднання передусім використовується для інтерфейсу з об'єднаннями в коді C. Доступ до полів об'єднання є небезпечним, бо Rust не може гарантувати, який саме тип даних зараз зберігається у екземплярі об'єднання. Більше про об'єднання ви можете дізнатися у Довіднику Rust.

Коли використовувати небезпечний код

Використання unsage для отримання однієї з п'яти дій (суперсил), про які ми щойно говорили, не є неправильним чи навіть несхвальним. Але код unsafe складніше зробити коректним, бо компілятор не може підтримувати безпеку пам'яті. Коли ви маєте причину використовувати unsafe, ви можете так робити, а наявність явних анотацій unsafe полегшує відстеження джерела проблем, коли вони виникають. ch04-02-references-and-borrowing.html#dangling-references ch03-01-variables-and-mutability.html#constants ch16-04-extensible-concurrency-sync-and-send.html#extensible-concurrency-with-the-sync-and-send-traits