Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Niebezpieczny Rust

Wszystkie omawiane do tej pory kody miały gwarancje bezpieczeństwa pamięci Rust egzekwowane w czasie kompilacji. Jednak Rust ma w sobie drugi, ukryty język, który nie egzekwuje tych gwarancji bezpieczeństwa pamięci: nazywa się go niebezpiecznym Rustem i działa dokładnie tak samo jak zwykły Rust, ale daje nam dodatkowe supermoce.

Niebezpieczny Rust istnieje, ponieważ z natury analiza statyczna jest konserwatywna. Kiedy kompilator próbuje określić, czy kod spełnia gwarancje, lepiej jest, aby odrzucił niektóre prawidłowe programy, niż zaakceptował niektóre nieprawidłowe programy. Chociaż kod może być w porządku, jeśli kompilator Rust nie ma wystarczających informacji, aby być pewnym, odrzuci kod. W takich przypadkach możesz użyć kodu niebezpiecznego, aby powiedzieć kompilatorowi: „Zaufaj mi, wiem, co robię”. Ostrzegamy jednak, że używasz niebezpiecznego Rust na własne ryzyko: jeśli użyjesz kodu niebezpiecznego niepoprawnie, mogą wystąpić problemy z powodu niebezpieczeństwa pamięci, takie jak dereferencja wskaźnika null.

Innym powodem, dla którego Rust ma swoje niebezpieczne alter ego, jest to, że podstawowy sprzęt komputerowy jest z natury niebezpieczny. Gdyby Rust nie pozwalał na wykonywanie niebezpiecznych operacji, nie można by wykonywać pewnych zadań. Rust musi pozwalać na programowanie systemów niskiego poziomu, takie jak bezpośrednia interakcja z systemem operacyjnym, a nawet pisanie własnego systemu operacyjnego. Praca z programowaniem systemów niskiego poziomu jest jednym z celów języka. Przyjrzyjmy się, co możemy zrobić z niebezpiecznym Rust i jak to zrobić.

Wykonywanie Niebezpiecznych Supermocy

Aby przełączyć się na niebezpieczny Rust, użyj słowa kluczowego unsafe, a następnie rozpocznij nowy blok, który zawiera niebezpieczny kod. W niebezpiecznym Rust możesz wykonać pięć akcji, których nie możesz w bezpiecznym Rust, które nazywamy niebezpiecznymi supermocami. Te supermoce obejmują zdolność do:

  1. Dereferencji surowego wskaźnika.
  2. Wywołania niebezpiecznej funkcji lub metody.
  3. Dostępu lub modyfikacji zmiennej statycznej zmiennej.
  4. Implementacji niebezpiecznej cechy.
  5. Dostępu do pól unionów.

Ważne jest, aby zrozumieć, że unsafe nie wyłącza sprawdzania pożyczania (borrow checker) ani nie wyłącza żadnych innych kontroli bezpieczeństwa Rust: jeśli użyjesz referencji w kodzie niebezpiecznym, nadal będzie ona sprawdzana. Słowo kluczowe unsafe daje jedynie dostęp do tych pięciu funkcji, które następnie nie są sprawdzane przez kompilator pod kątem bezpieczeństwa pamięci. Nadal uzyskasz pewien stopień bezpieczeństwa wewnątrz bloku unsafe.

Ponadto, unsafe nie oznacza, że kod wewnątrz bloku jest koniecznie niebezpieczny lub że na pewno będzie miał problemy z bezpieczeństwem pamięci: intencją jest, aby jako programista zapewnił, że kod wewnątrz bloku unsafe będzie miał dostęp do pamięci w prawidłowy sposób.

Ludzie są omylni i błędy się zdarzą, ale wymagając, aby te pięć niebezpiecznych operacji znajdowało się w blokach opatrzonych adnotacją unsafe, będziesz wiedzieć, że wszelkie błędy związane z bezpieczeństwem pamięci muszą znajdować się w bloku unsafe. Pamiętaj, aby bloki unsafe były małe; będziesz za to wdzięczny później, gdy będziesz badać błędy pamięci.

Aby jak najbardziej izolować niebezpieczny kod, najlepiej jest umieścić go w bezpiecznej abstrakcji i udostępnić bezpieczne API, co omówimy później w rozdziale, gdy będziemy badać niebezpieczne funkcje i metody. Części biblioteki standardowej są implementowane jako bezpieczne abstrakcje nad niebezpiecznym kodem, który został poddany audytowi. Opakowanie niebezpiecznego kodu w bezpieczną abstrakcję zapobiega wyciekaniu użycia unsafe do wszystkich miejsc, w których Ty lub Twoi użytkownicy moglibyście chcieć użyć funkcjonalności zaimplementowanej za pomocą kodu unsafe, ponieważ użycie bezpiecznej abstrakcji jest bezpieczne.

Przyjrzyjmy się z kolei każdej z pięciu niebezpiecznych supermocy. Przyjrzymy się również niektórym abstrakcjom, które zapewniają bezpieczny interfejs do niebezpiecznego kodu.

Dereferencja Surowego Wskaźnika

W Rozdziale 4, w sekcji „Wiszące referencje”, wspomnieliśmy, że kompilator zapewnia, że referencje są zawsze prawidłowe. Niebezpieczny Rust ma dwa nowe typy zwane surowymi wskaźnikami, które są podobne do referencji. Podobnie jak referencje, surowe wskaźniki mogą być niemodyfikowalne lub modyfikowalne i są zapisywane odpowiednio jako *const T i *mut T. Gwiazdka nie jest operatorem dereferencji; jest częścią nazwy typu. W kontekście surowych wskaźników, niemodyfikowalne oznacza, że wskaźnik nie może być bezpośrednio przypisany po dereferencji.

Różniące się od referencji i inteligentnych wskaźników, surowe wskaźniki:

  • Mogą ignorować zasady pożyczania, posiadając zarówno niemodyfikowalne, jak i modyfikowalne wskaźniki, lub wiele modyfikowalnych wskaźników do tej samej lokalizacji
  • Nie mają gwarancji, że wskazują na prawidłową pamięć
  • Mogą być null
  • Nie implementują żadnego automatycznego czyszczenia

Rezygnując z egzekwowania tych gwarancji przez Rust, możesz zrezygnować z gwarantowanego bezpieczeństwa w zamian za większą wydajność lub możliwość współpracy z innym językiem lub sprzętem, gdzie gwarancje Rust nie mają zastosowania.

Lista 20-1 pokazuje, jak utworzyć niemodyfikowalny i modyfikowalny surowy wskaźnik.

fn main() {
    let mut num = 5;

    let r1 = &raw const num;
    let r2 = &raw mut num;
}
Lista 20-1: Tworzenie surowych wskaźników za pomocą operatorów surowego pożyczania

Zauważ, że w tym kodzie nie używamy słowa kluczowego unsafe. Możemy tworzyć surowe wskaźniki w bezpiecznym kodzie; po prostu nie możemy dereferencjonować surowych wskaźników poza blokiem unsafe, jak zobaczysz za chwilę.

Stworzyliśmy surowe wskaźniki za pomocą operatorów surowego pożyczania: &raw const num tworzy niemodyfikowalny surowy wskaźnik *const i32, a &raw mut num tworzy modyfikowalny surowy wskaźnik *mut i32. Ponieważ stworzyliśmy je bezpośrednio ze zmiennej lokalnej, wiemy, że te konkretne surowe wskaźniki są prawidłowe, ale nie możemy zakładać tego samego o każdym surowym wskaźniku.

Aby to zademonstrować, następnie stworzymy surowy wskaźnik, którego ważności nie możemy być tak pewni, używając słowa kluczowego as do rzutowania wartości zamiast operatora surowego pożyczenia. Lista 20-2 pokazuje, jak stworzyć surowy wskaźnik do dowolnej lokalizacji w pamięci. Próba użycia dowolnej pamięci jest niezdefiniowana: pod tym adresem mogą być dane lub nie, kompilator może zoptymalizować kod tak, że nie ma dostępu do pamięci, lub program może zakończyć się błędem segmentacji. Zazwyczaj nie ma dobrego powodu do pisania takiego kodu, zwłaszcza w przypadkach, gdy zamiast tego można użyć operatora surowego pożyczenia, ale jest to możliwe.

fn main() {
    let address = 0x012345usize;
    let r = address as *const i32;
}
Lista 20-2: Tworzenie surowego wskaźnika do dowolnego adresu pamięci

Przypomnij sobie, że możemy tworzyć surowe wskaźniki w bezpiecznym kodzie, ale nie możemy ich dereferencjonować i odczytywać wskazywanych danych. Na Liście 20-3 używamy operatora dereferencji * na surowym wskaźniku, co wymaga bloku 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);
    }
}
Lista 20-3: Dereferencja surowych wskaźników wewnątrz bloku unsafe

Tworzenie wskaźnika nie szkodzi; dopiero gdy próbujemy uzyskać dostęp do wartości, na którą wskazuje, możemy skończyć z nieprawidłową wartością.

Zauważ również, że na Liście 20-1 i 20-3 stworzyliśmy surowe wskaźniki *const i32 i *mut i32, które oba wskazywały na tę samą lokalizację w pamięci, gdzie przechowywany jest num. Gdybyśmy zamiast tego spróbowali stworzyć niemodyfikowalną i modyfikowalną referencję do num, kod nie skompilowałby się, ponieważ zasady własności Rust nie pozwalają na jednoczesne istnienie modyfikowalnej referencji i niemodyfikowalnych referencji. Z surowymi wskaźnikami możemy stworzyć modyfikowalny wskaźnik i niemodyfikowalny wskaźnik do tej samej lokalizacji i zmieniać dane za pomocą modyfikowalnego wskaźnika, potencjalnie tworząc wyścig danych. Bądź ostrożny!

Przy wszystkich tych niebezpieczeństwach, dlaczego w ogóle miałbyś używać surowych wskaźników? Jednym z głównych przypadków użycia jest interakcja z kodem C, jak zobaczysz w następnej sekcji. Innym przypadkiem jest budowanie bezpiecznych abstrakcji, których sprawdzający pożyczanie nie rozumie. Przedstawimy niebezpieczne funkcje, a następnie przyjrzymy się przykładowi bezpiecznej abstrakcji, która używa niebezpiecznego kodu.

Wywoływanie Niebezpiecznej Funkcji lub Metody

Drugi rodzaj operacji, którą można wykonać w bloku unsafe, to wywołanie niebezpiecznych funkcji. Niebezpieczne funkcje i metody wyglądają dokładnie tak samo jak zwykłe funkcje i metody, ale mają dodatkowe unsafe przed resztą definicji. Słowo kluczowe unsafe w tym kontekście wskazuje, że funkcja ma wymagania, które musimy spełnić, gdy ją wywołujemy, ponieważ Rust nie może zagwarantować, że spełniliśmy te wymagania. Wywołując niebezpieczną funkcję w bloku unsafe, mówimy, że przeczytaliśmy dokumentację tej funkcji i bierzemy odpowiedzialność za przestrzeganie jej kontraktów.

Oto niebezpieczna funkcja o nazwie dangerous, która nic nie robi w swoim ciele:

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

    unsafe {
        dangerous();
    }
}

Musimy wywołać funkcję dangerous w osobnym bloku unsafe. Jeśli spróbujemy wywołać dangerous bez bloku unsafe, otrzymamy błąd:

$ 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

Dzięki blokowi unsafe zapewniamy Rust, że przeczytaliśmy dokumentację funkcji, rozumiemy, jak jej właściwie używać, i zweryfikowaliśmy, że spełniamy kontrakt funkcji.

Aby wykonywać niebezpieczne operacje w ciele funkcji unsafe, nadal musisz użyć bloku unsafe, tak jak w zwykłej funkcji, a kompilator ostrzeże Cię, jeśli zapomnisz. Pomaga to nam utrzymywać bloki unsafe tak małe, jak to możliwe, ponieważ operacje niebezpieczne mogą nie być potrzebne w całym ciele funkcji.

Tworzenie Bezpiecznej Abstrakcji nad Niebezpiecznym Kodem

To, że funkcja zawiera niebezpieczny kod, nie oznacza, że musimy oznaczyć całą funkcję jako niebezpieczną. W rzeczywistości, opakowywanie niebezpiecznego kodu w bezpieczną funkcję jest powszechną abstrakcją. Jako przykład, przeanalizujmy funkcję split_at_mut z biblioteki standardowej, która wymaga pewnego niebezpiecznego kodu. Zbadamy, jak moglibyśmy ją zaimplementować. Ta bezpieczna metoda jest zdefiniowana dla zmiennych wycinków: bierze jeden wycinek i tworzy z niego dwa, dzieląc wycinek na indeksie podanym jako argument. Lista 20-4 pokazuje, jak używać 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]);
}
Lista 20-4: Użycie bezpiecznej funkcji split_at_mut

Nie możemy zaimplementować tej funkcji używając wyłącznie bezpiecznego Rust. Próba mogłaby wyglądać mniej więcej jak Lista 20-5, która się nie skompiluje. Dla uproszczenia, zaimplementujemy split_at_mut jako funkcję, a nie metodę, i tylko dla wycinków wartości i32, a nie dla generycznego typu 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);
}
Lista 20-5: Próba implementacji split_at_mut używając wyłącznie bezpiecznego Rust

Ta funkcja najpierw pobiera całkowitą długość wycinka. Następnie sprawdza, czy indeks podany jako parametr mieści się w wycinku, sprawdzając, czy jest mniejszy lub równy długości. Asercja oznacza, że jeśli przekażemy indeks większy niż długość do podziału wycinka, funkcja spanikuje, zanim spróbuje użyć tego indeksu.

Następnie zwracamy dwa modyfikowalne wycinki w krotce: jeden od początku oryginalnego wycinka do indeksu mid i drugi od mid do końca wycinka.

Kiedy spróbujemy skompilować kod z Listy 20-5, otrzymamy błąd:

$ 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

Sprawdzający pożyczanie Rust nie może zrozumieć, że pożyczamy różne części wycinka; wie tylko, że pożyczamy z tego samego wycinka dwa razy. Pożyczanie różnych części wycinka jest zasadniczo w porządku, ponieważ te dwa wycinki nie nakładają się na siebie, ale Rust nie jest na tyle sprytny, aby to wiedzieć. Kiedy wiemy, że kod jest w porządku, ale Rust nie, nadszedł czas, aby sięgnąć po niebezpieczny kod.

Lista 20-6 pokazuje, jak użyć bloku unsafe, surowego wskaźnika i kilku wywołań niebezpiecznych funkcji, aby implementacja split_at_mut działała.

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);
}
Lista 20-6: Użycie niebezpiecznego kodu w implementacji funkcji split_at_mut

Przypomnij sobie z sekcji „Typ wycinka” w Rozdziale 4, że wycinek jest wskaźnikiem do pewnych danych i długością wycinka. Używamy metody len, aby uzyskać długość wycinka, i metody as_mut_ptr, aby uzyskać dostęp do surowego wskaźnika wycinka. W tym przypadku, ponieważ mamy modyfikowalny wycinek wartości i32, as_mut_ptr zwraca surowy wskaźnik typu *mut i32, który zapisaliśmy w zmiennej ptr.

Utrzymujemy asercję, że indeks mid znajduje się w zakresie wycinka. Następnie przechodzimy do kodu niebezpiecznego: funkcja slice::from_raw_parts_mut przyjmuje surowy wskaźnik i długość i tworzy wycinek. Używamy tej funkcji do stworzenia wycinka, który zaczyna się od ptr i ma długość mid elementów. Następnie wywołujemy metodę add na ptr z mid jako argumentem, aby uzyskać surowy wskaźnik, który zaczyna się na mid, i tworzymy wycinek używając tego wskaźnika i pozostałej liczby elementów po mid jako długości.

Funkcja slice::from_raw_parts_mut jest niebezpieczna, ponieważ przyjmuje surowy wskaźnik i musi ufać, że ten wskaźnik jest prawidłowy. Metoda add na surowych wskaźnikach jest również niebezpieczna, ponieważ musi ufać, że lokalizacja offsetu jest również prawidłowym wskaźnikiem. Dlatego musieliśmy umieścić blok unsafe wokół naszych wywołań slice::from_raw_parts_mut i add, aby móc je wywołać. Patrząc na kod i dodając asercję, że mid musi być mniejsze lub równe len, możemy stwierdzić, że wszystkie surowe wskaźniki użyte w bloku unsafe będą prawidłowe i będą wskazywać na dane wewnątrz wycinka. Jest to dopuszczalne i odpowiednie użycie unsafe.

Zauważ, że nie musimy oznaczać wynikowej funkcji split_at_mut jako unsafe, a możemy wywołać tę funkcję z bezpiecznego Rust. Stworzyliśmy bezpieczną abstrakcję dla niebezpiecznego kodu z implementacją funkcji, która używa kodu unsafe w bezpieczny sposób, ponieważ tworzy tylko prawidłowe wskaźniki z danych, do których ta funkcja ma dostęp.

W przeciwieństwie do tego, użycie slice::from_raw_parts_mut na Liście 20-7 prawdopodobnie spowodowałoby awarię programu, gdy wycinek zostałby użyty. Ten kod pobiera dowolną lokalizację w pamięci i tworzy wycinek o długości 10 000 elementów.

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) };
}
Lista 20-7: Tworzenie wycinka z dowolnej lokalizacji w pamięci

Nie posiadamy pamięci w tej dowolnej lokalizacji i nie ma gwarancji, że wycinek, który ten kod tworzy, zawiera prawidłowe wartości i32. Próba użycia values tak, jakby był to prawidłowy wycinek, prowadzi do niezdefiniowanego zachowania.

Używanie funkcji extern do wywoływania zewnętrznego kodu

Czasami kod w Rust może potrzebować interakcji z kodem napisanym w innym języku. W tym celu Rust posiada słowo kluczowe extern, które ułatwia tworzenie i używanie interfejsu funkcji obcych (FFI), czyli sposobu, w jaki język programowania może definiować funkcje i umożliwiać innemu (obcemu) językowi programowania wywoływanie tych funkcji.

Lista 20-8 demonstruje, jak skonfigurować integrację z funkcją abs z biblioteki standardowej C. Funkcje zadeklarowane w blokach extern są zazwyczaj niebezpieczne do wywoływania z kodu Rust, dlatego bloki extern muszą być również oznaczone jako unsafe. Powodem jest to, że inne języki nie egzekwują zasad i gwarancji Rust, a Rust nie może ich sprawdzić, więc odpowiedzialność za zapewnienie bezpieczeństwa spoczywa na programiście.

Nazwa pliku: src/main.rs
unsafe extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Absolute value of -3 according to C: {}", abs(-3));
    }
}
Lista 20-8: Deklarowanie i wywoływanie funkcji extern zdefiniowanej w innym języku

W bloku unsafe extern "C" wymieniamy nazwy i sygnatury funkcji zewnętrznych z innego języka, które chcemy wywołać. Część "C" definiuje, który interfejs binarny aplikacji (ABI) używa funkcja zewnętrzna: ABI definiuje, jak wywołać funkcję na poziomie asemblera. ABI "C" jest najpopularniejsze i jest zgodne z ABI języka programowania C. Informacje o wszystkich ABI obsługiwanych przez Rust są dostępne w referencji Rust.

Każdy element zadeklarowany w bloku unsafe extern jest domyślnie niebezpieczny. Jednak niektóre funkcje FFI bezpieczne do wywołania. Na przykład funkcja abs z biblioteki standardowej C nie ma żadnych ograniczeń bezpieczeństwa pamięci i wiemy, że można ją wywołać z dowolną i32. W takich przypadkach możemy użyć słowa kluczowego safe, aby powiedzieć, że ta konkretna funkcja jest bezpieczna do wywołania, nawet jeśli znajduje się w bloku unsafe extern. Po dokonaniu tej zmiany, wywołanie jej nie wymaga już bloku unsafe, jak pokazano na Liście 20-9.

Nazwa pliku: src/main.rs
unsafe extern "C" {
    safe fn abs(input: i32) -> i32;
}

fn main() {
    println!("Absolute value of -3 according to C: {}", abs(-3));
}
Lista 20-9: Jawne oznaczanie funkcji jako safe w bloku unsafe extern i bezpieczne jej wywoływanie

Oznaczenie funkcji jako safe nie czyni jej z natury bezpieczną! Zamiast tego, jest to obietnica, którą składasz Rust, że jest bezpieczna. Nadal Twoim obowiązkiem jest upewnienie się, że ta obietnica jest dotrzymana!

Wywoływanie funkcji Rust z innych języków

Możemy również użyć extern do stworzenia interfejsu, który pozwala innym językom wywoływać funkcje Rust. Zamiast tworzyć cały blok extern, dodajemy słowo kluczowe extern i określamy ABI do użycia tuż przed słowem kluczowym fn dla odpowiedniej funkcji. Musimy również dodać adnotację #[unsafe(no_mangle)], aby powiedzieć kompilatorowi Rust, aby nie zmieniał nazwy tej funkcji. Mangling to proces, w którym kompilator zmienia nazwę, którą nadaliśmy funkcji, na inną nazwę, która zawiera więcej informacji dla innych części procesu kompilacji, ale jest mniej czytelna dla człowieka. Każdy kompilator języka programowania nieco inaczej zniekształca nazwy, więc aby funkcja Rust mogła być nazwana przez inne języki, musimy wyłączyć zniekształcanie nazw przez kompilator Rust. Jest to niebezpieczne, ponieważ bez wbudowanego zniekształcania mogą występować kolizje nazw w bibliotekach, więc naszym obowiązkiem jest upewnienie się, że wybrana nazwa jest bezpieczna do eksportu bez zniekształcania.

W poniższym przykładzie udostępniamy funkcję call_from_c z kodu C, po skompilowaniu jej do biblioteki współdzielonej i połączeniu z C:

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

To użycie extern wymaga unsafe tylko w atrybucie, a nie w bloku extern.

Dostęp do Zmiennej Statycznej Modyfikowalnej lub Jej Modyfikowanie

W tej książce nie mówiliśmy jeszcze o zmiennych globalnych, które Rust obsługuje, ale które mogą być problematyczne z zasadami własności Rust. Jeśli dwa wątki uzyskują dostęp do tej samej zmiennej globalnej, może to spowodować wyścig danych.

W Rust zmienne globalne nazywane są zmiennymi statycznymi. Lista 20-10 pokazuje przykład deklaracji i użycia zmiennej statycznej z fragmentem ciągu jako wartością.

Nazwa pliku: src/main.rs
static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("value is: {HELLO_WORLD}");
}
Lista 20-10: Definiowanie i używanie niemodyfikowalnej zmiennej statycznej

Zmienne statyczne są podobne do stałych, które omówiliśmy w sekcji „Deklarowanie stałych” w Rozdziale 3. Nazwy zmiennych statycznych są konwencjonalnie zapisywane w formacie SCREAMING_SNAKE_CASE. Zmienne statyczne mogą przechowywać tylko referencje z czasem życia 'static, co oznacza, że kompilator Rust może określić czas życia i nie musimy go jawnie adnotować. Dostęp do niemodyfikowalnej zmiennej statycznej jest bezpieczny.

Subtelna różnica między stałymi a niemodyfikowalnymi zmiennymi statycznymi polega na tym, że wartości w zmiennej statycznej mają stały adres w pamięci. Użycie wartości zawsze będzie odwoływać się do tych samych danych. Stałe natomiast mogą duplikować swoje dane za każdym razem, gdy są używane. Inną różnicą jest to, że zmienne statyczne mogą być zmienne. Dostęp i modyfikacja zmiennych statycznych zmiennych jest niebezpieczna. Lista 20-11 pokazuje, jak zadeklarować, uzyskać dostęp i zmodyfikować zmienną statyczną zmienną o nazwie COUNTER.

Nazwa pliku: src/main.rs
static mut COUNTER: u32 = 0;

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

fn main() {
    unsafe {
        // SAFETY: This is only called from a single thread in `main`.
        add_to_count(3);
        println!("COUNTER: {}", *(&raw const COUNTER));
    }
}
Lista 20-11: Odczytywanie lub zapisywanie do zmiennej statycznej zmiennej jest niebezpieczne.

Podobnie jak w przypadku zwykłych zmiennych, mutowalność określamy za pomocą słowa kluczowego mut. Każdy kod, który odczytuje lub zapisuje z COUNTER, musi znajdować się w bloku unsafe. Kod z Listy 20-11 kompiluje się i wypisuje COUNTER: 3, tak jak byśmy się spodziewali, ponieważ jest jednowątkowy. Dostęp do COUNTER z wielu wątków prawdopodobnie skutkowałby wyścigami danych, więc jest to niezdefiniowane zachowanie. Dlatego musimy oznaczyć całą funkcję jako unsafe i udokumentować ograniczenie bezpieczeństwa, aby każdy, kto wywołuje funkcję, wiedział, co mu wolno, a czego nie wolno bezpiecznie robić.

Zawsze, gdy piszemy funkcję unsafe, idiomatyczne jest umieszczenie komentarza zaczynającego się od SAFETY i wyjaśniającego, co wywołujący musi zrobić, aby bezpiecznie wywołać funkcję. Podobnie, zawsze, gdy wykonujemy operację unsafe, idiomatyczne jest umieszczenie komentarza zaczynającego się od SAFETY, aby wyjaśnić, w jaki sposób zasady bezpieczeństwa są przestrzegane.

Ponadto, kompilator domyślnie odrzuci wszelkie próby tworzenia referencji do zmiennej statycznej zmiennej za pomocą lintu kompilatora. Musisz albo jawnie zrezygnować z ochrony tego lintu, dodając adnotację #[allow(static_mut_refs)], albo uzyskać dostęp do zmiennej statycznej zmiennej za pośrednictwem surowego wskaźnika utworzonego za pomocą jednego z operatorów surowego pożyczania. Obejmuje to przypadki, w których referencja jest tworzona niewidocznie, jak w przypadku jej użycia w println! w tej liście kodu. Wymaganie, aby referencje do zmiennych statycznych zmiennych były tworzone za pośrednictwem surowych wskaźników, pomaga uczynić wymagania bezpieczeństwa ich użycia bardziej oczywistymi.

Przy zmiennych danych, które są globalnie dostępne, trudno jest zapewnić, że nie ma wyścigów danych, dlatego Rust uważa zmienne statyczne zmienne za niebezpieczne. Tam, gdzie to możliwe, lepiej jest używać technik współbieżności i inteligentnych wskaźników bezpiecznych dla wątków, które omówiliśmy w Rozdziale 16, aby kompilator sprawdzał, czy dostęp do danych z różnych wątków odbywa się bezpiecznie.

Implementowanie Niebezpiecznej Cechy

Możemy użyć unsafe do zaimplementowania niebezpiecznej cechy. Cecha jest niebezpieczna, gdy co najmniej jedna z jej metod ma jakąś niezmienną, której kompilator nie może zweryfikować. Deklarujemy, że cecha jest unsafe, dodając słowo kluczowe unsafe przed trait i oznaczając implementację cechy jako unsafe, jak pokazano na Liście 20-12.

unsafe trait Foo {
    // methods go here
}

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

fn main() {}
Lista 20-12: Definiowanie i implementacja niebezpiecznej cechy

Używając unsafe impl, obiecujemy, że będziemy przestrzegać niezmiennych, których kompilator nie może zweryfikować.

Jako przykład, przypomnijmy cechy znaczników Send i Sync, które omówiliśmy w sekcji „Rozszerzalna współbieżność z Send i Sync w Rozdziale 16: kompilator implementuje te cechy automatycznie, jeśli nasze typy składają się wyłącznie z innych typów, które implementują Send i Sync. Jeśli zaimplementujemy typ zawierający typ, który nie implementuje Send lub Sync, taki jak surowe wskaźniki, i chcemy oznaczyć ten typ jako Send lub Sync, musimy użyć unsafe. Rust nie może zweryfikować, czy nasz typ spełnia gwarancje, że może być bezpiecznie przesyłany między wątkami lub dostępny z wielu wątków; dlatego musimy ręcznie wykonać te sprawdzenia i wskazać to za pomocą unsafe.

Dostęp do Pól Unii

Ostatnią akcją, która działa tylko z unsafe, jest dostęp do pól unii. Unia jest podobna do struct, ale tylko jedno zadeklarowane pole jest używane w danej instancji w danym momencie. Unie są używane głównie do interfejsu z uniami w kodzie C. Dostęp do pól unii jest niebezpieczny, ponieważ Rust nie może zagwarantować typu danych aktualnie przechowywanych w instancji unii. Możesz dowiedzieć się więcej o uniach w referencji Rust.

Używanie Miri do Sprawdzania Niebezpiecznego Kodu

Podczas pisania kodu niebezpiecznego, możesz chcieć sprawdzić, czy to, co napisałeś, jest faktycznie bezpieczne i poprawne. Jednym z najlepszych sposobów na to jest użycie Miri, oficjalnego narzędzia Rust do wykrywania niezdefiniowanego zachowania. Podczas gdy sprawdzający pożyczanie (borrow checker) jest narzędziem statycznym, które działa w czasie kompilacji, Miri jest narzędziem dynamicznym, które działa w czasie wykonania. Sprawdza Twój kod, uruchamiając Twój program lub jego pakiet testowy i wykrywając, kiedy naruszasz zasady, które rozumie, jak Rust powinien działać.

Używanie Miri wymaga nocnej (nightly) wersji Rust (o której więcej mówimy w Dodatku G: Jak powstaje Rust i „Nocny Rust”). Możesz zainstalować zarówno nocną wersję Rust, jak i narzędzie Miri, wpisując rustup +nightly component add miri. Nie zmienia to wersji Rust używanej w Twoim projekcie; dodaje tylko narzędzie do Twojego systemu, abyś mógł go używać, kiedy zechcesz. Miri możesz uruchomić na projekcie, wpisując cargo +nightly miri run lub cargo +nightly miri test.

Na przykładzie, jak bardzo to może być pomocne, rozważmy, co się dzieje, gdy uruchomimy go na Liście 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 poprawnie ostrzega nas, że rzutujemy liczbę całkowitą na wskaźnik, co może być problemem, ale Miri nie może ustalić, czy problem istnieje, ponieważ nie wie, skąd pochodzi wskaźnik. Następnie Miri zwraca błąd, gdzie Lista 20-7 ma niezdefiniowane zachowanie, ponieważ mamy wiszący wskaźnik. Dzięki Miri wiemy teraz, że istnieje ryzyko niezdefiniowanego zachowania i możemy zastanowić się, jak uczynić kod bezpiecznym. W niektórych przypadkach Miri może nawet zalecić, jak naprawić błędy.

Miri nie wychwytuje wszystkiego, co możesz zrobić źle podczas pisania niebezpiecznego kodu. Miri to narzędzie do analizy dynamicznej, więc wykrywa problemy tylko w kodzie, który faktycznie zostaje uruchomiony. Oznacza to, że będziesz musiał używać go w połączeniu z dobrymi technikami testowania, aby zwiększyć swoją pewność co do napisanego niebezpiecznego kodu. Miri nie obejmuje również wszystkich możliwych sposobów, w jaki Twój kod może być niestabilny.

Inaczej mówiąc: jeśli Miri znajdzie problem, wiesz, że jest błąd, ale to, że Miri nie znajdzie błędu, nie oznacza, że problemu nie ma. Może jednak wychwycić wiele. Spróbuj uruchomić go na innych przykładach niebezpiecznego kodu w tym rozdziale i zobacz, co powie!

Więcej o Miri dowiesz się w repozytorium GitHub.

Poprawne Użycie Kodu Niebezpiecznego

Użycie unsafe do wykorzystania jednej z pięciu omówionych właśnie supermocy nie jest błędem ani nawet czymś, na co patrzy się krzywo, ale poprawne napisanie kodu unsafe jest trudniejsze, ponieważ kompilator nie może pomóc w utrzymaniu bezpieczeństwa pamięci. Kiedy masz powód, aby użyć kodu unsafe, możesz to zrobić, a jawna adnotacja unsafe ułatwia śledzenie źródła problemów, gdy się pojawią. Zawsze, gdy piszesz kod unsafe, możesz użyć Miri, aby zwiększyć pewność, że napisany kod przestrzega zasad Rust.

Aby uzyskać znacznie głębsze poznanie, jak efektywnie pracować z niebezpiecznym Rust, przeczytaj oficjalny przewodnik Rust dotyczący unsafe, The Rustonomicon.