unsafe și pointeri bruți în Rust

Cod unsafe, FFI și async Rust 1.0 (&raw const / &raw mut: 1.82)

unsafe permite cinci operații pe care compilatorul nu le poate verifica, ca dereferențierea pointerilor bruți; corectitudinea o garantezi tu.

Sintaxă
let p: *const T = &raw const x;   // pointer brut, doar citire
let m: *mut T = &raw mut y;       // pointer brut, cu scriere
unsafe { *m = 5; }                // dereferențierea cere unsafe
unsafe fn periculoasa() { }       // apelul cere unsafe
unsafe impl Send for T { }

Ce face unsafe și pointeri bruți

Codul Rust obișnuit este verificat complet de compilator. Un bloc unsafe { ... } permite în plus cinci operații: dereferențierea unui pointer brut (*const T, *mut T), apelul unei funcții unsafe (inclusiv funcții C), accesul la un static mut, implementarea unui trait unsafe și accesul la câmpurile unui union. Restul regulilor (tipuri, ownership pentru referințe) rămân active.

Pointerii bruți seamănă cu pointerii din C: pot fi nuli, pot arăta spre memorie eliberată și nu respectă regulile de împrumut. Crearea lor este sigură (&raw const x, &raw mut x sau &x as *const T); doar folosirea lor cere unsafe. În unsafe, tu garantezi că pointerul este valid; dacă greșești, comportamentul este nedefinit, exact ca în C.

Bune practici: ține blocurile unsafe cât mai mici, ascunde-le în spatele unei funcții sigure și scrie deasupra un comentariu // SAFETY: care explică de ce codul este corect. O funcție unsafe fn documentează în secțiunea # Safety ce trebuie să garanteze apelantul. În ediția 2024, chiar și în corpul unei unsafe fn operațiile nesigure trebuie puse într-un bloc unsafe { }.

Exemple

Exemplul 1: Pointeri bruți: creare și dereferențiere

Crearea pointerilor nu cere unsafe; citirea și scrierea prin ei, da. Pointerul nul poate fi verificat cu is_null() înainte de folosire.

fn main() {
    let mut stoc = 10;
    let p = &raw mut stoc;
    let nul: *const i32 = std::ptr::null();

    // SAFETY: p arată spre `stoc`, care trăiește până la sfârșitul lui main
    // și nu este folosit prin altă referință în acest timp.
    unsafe {
        *p -= 3;
        println!("Stoc prin pointer: {}", *p);
    }
    println!("Stoc: {stoc}");
    println!("Pointerul nul este nul: {}", nul.is_null());
}

Rezultat

Stoc prin pointer: 7
Stoc: 7
Pointerul nul este nul: true

Exemplul 2: O funcție sigură construită peste cod unsafe

imparte_in_doua returnează două slice-uri mutabile din același vector, lucru pe care borrow checker-ul nu îl poate dovedi singur. Funcția verifică întâi indicele, deci apelantul nu poate produce o eroare de memorie: interfața ei este sigură. (Biblioteca standard oferă exact asta ca split_at_mut.)

use std::slice;

fn imparte_in_doua(v: &mut [u32], mijloc: usize) -> (&mut [u32], &mut [u32]) {
    assert!(mijloc <= v.len());
    let lungime = v.len();
    let p = v.as_mut_ptr();
    // SAFETY: cele două bucăți nu se suprapun ([0, mijloc) și [mijloc, lungime))
    // și amândouă sunt în interiorul lui `v`, verificat de assert.
    unsafe {
        (
            slice::from_raw_parts_mut(p, mijloc),
            slice::from_raw_parts_mut(p.add(mijloc), lungime - mijloc),
        )
    }
}

fn main() {
    let mut vanzari = vec![10, 20, 30, 40, 50];
    let (sem1, sem2) = imparte_in_doua(&mut vanzari, 2);
    sem1[0] += 1;
    sem2[0] += 100;
    println!("{vanzari:?}");
}

Rezultat

[11, 20, 130, 40, 50]

Exemplul 3: unsafe fn cu secțiunea Safety

element_neverificat nu verifică indicele, deci este unsafe fn: apelantul trebuie să garanteze că indicele este valid. Și în corpul ei, operația nesigură stă într-un bloc unsafe, cum cere ediția 2024.

/// Returnează elementul de la poziția `i`, fără verificare.
///
/// # Safety
/// `i` trebuie să fie mai mic decât `date.len()`.
unsafe fn element_neverificat(date: &[u8], i: usize) -> u8 {
    // SAFETY: apelantul garantează că i < date.len().
    unsafe { *date.get_unchecked(i) }
}

fn main() {
    let note = [7, 9, 10, 8];
    let mut suma = 0;
    for i in 0..note.len() {
        // SAFETY: i merge de la 0 la note.len() - 1.
        suma += unsafe { element_neverificat(&note, i) } as u32;
    }
    println!("Suma notelor: {suma}");
}

Rezultat

Suma notelor: 34

Greșeli frecvente

  • Crezi că unsafe dezactivează toate verificările. Nu: tipurile și regulile de împrumut pentru referințe se verifică și acolo; se deblochează doar cele cinci operații.
  • Faci o funcție publică „sigură” care, cu anumite argumente, produce comportament nedefinit (de exemplu fără assert!-ul din exemplul 2). O funcție fără unsafe în semnătură trebuie să fie corectă pentru orice argumente.
  • Folosești unsafe pentru performanță fără măsurători. Verificările de limite sunt de obicei eliminate oricum de optimizator; iteratorii (iter()) sunt la fel de rapizi și siguri.
  • Creezi referințe la un static mut (&CONTOR): în ediția 2024 este eroare. Folosește tipuri atomice, Mutex sau &raw const CONTOR.

Comenzi înrudite

  • extern (funcții C și FFI): extern leagă Rust de cod scris în alte limbaje: declară funcții C de apelat din Rust și funcții Rust cu convenția de apel C.
  • const și static: const definește o valoare fixă calculată la compilare; static definește o variabilă globală cu o singură adresă, care trăiește tot programul.
  • borrowing și referințe (& și &mut): O referință (&) împrumută o valoare fără să o ia de la proprietar. Regula: oricâte referințe de citire SAU una singură de modificare.
  • slice-uri (&[T] și &str): O „fereastră” spre o parte dintr-un array, Vec sau text: o referință care știe de unde începe și câte elemente are, fără să copieze nimic.