Memória kezelés Rust nyelvvel

Szecsődi Imre

Mi az a Rust?

  • Systems Programming language
  • Szintaxisban hasonlít a C++-ra
  • Gyors mint a C++
  • Mozilla Research projekt
  • Legszeretettebb nyelv Stack Overflown 2016 óta

Mi íródott Rust-ban?

  • Rust compiler

Mi íródott Rust-ban?

  • Servo layout engine

Memória kezelési módszerek

  • Kézi foglalás és felszabadítás - C/C++
  • Szemétgyűjtés - Java, Python, stb.
  • Tulajdonos alapú kezelés - Rust

Kézi memória kezelés C/C++

  • Azok az értékek aminek futás időben ismert a mérete azok a stacken fognak létrejönni, ide tartoznak a primitív változók
  • Amit dinamikusan hozunk létre, azok a heapen fognak létrejönni, amit majd fel kell onnan szabadítani
  • Attól függően, hogy smart vagy raw pointerről beszélünk, az elsőnél a program gondoskodik majd a felszabadításról, utóbbi esetben nekünk kell azt kézzel

Kézi memória kezelés C/C++

int main() {
    int i = 32;

    char (*pchar)[10] = new char[dim][10];
    delete [] pchar;
}

Szemétgyűjtés - Java

  • Korábbi órákon esett róla szó
  • A program futás időben nyilvántartja az objektumokat, és mikor véget ér az objektum élettartama, akkor meg lesz jelölve törlésre, és a szemétgyűjtéskor fel fog szabadulni

Tulajdonos alapú - Rust

  • Alapötlet: csak azokat az objektumokat tudjuk garantálni, hogy felszabadulnak amik stacken helyezkednek el
  • Minden objektumhoz ami létrejön heapen, tartozik egy minimális objektum a stacken is
  • Ezért objektumoknak csak egy tulajdonosuk lehet egy időben

Fordítóval való harc

  • Mikor először írunk Rust kódot, fel fog tűnni, hogy a fordító nagyon sok mindenre hibát fog jelezni, a borrow checker miatt
  • A fordító elvárja, hogy a program memória biztosan legyen megírva, így sok hibát már fordítás időben kiküszöbölhetünk:
    • Buffer túlcsordulás
    • Felszabadítás utáni használat
    • Nem inicializált változók használata
    • Null pointer használata
    • Kétszeri felszabadítás, stb.

Fordítóval való harc

  • Ezeket a dolgokat statikus elemzéssel a fordító még időnap előtt megfogja, és egy hasznos hibaüzenettel közli velünk a problémát, és javaslatot is ad javításra
  • A Rust az biztonságos nyelv, még betartjuk ezeket a szabályokat, de van lehetőség \(unsafe\) blokk írásra, ahol nem lesz ellenőrizve, és ott előjöhetnek problémát futás időben
  • Ez főleg külső C könyvtáraknál fordul elő
  • A Rust biztonságossága csak memóriára vonatkozik, nem véd erőforrás szivárgás ellen, pl. nyitva maradt socket handler

Hol jönnek létre a dolgok?

  • Aminek ismert fordítás időben a mérete, azok mind a stacken jönnek létre, ezzel garantálva, hogy mikor kilépünk a blokkból, stack kiürítésekor rögtön felszabadul minden
  • Dinamikus métertű objektumok a heapen jönnek létre, ide tartozik a String struktura, és a konténerek
  • A tulajdonos alapú memória kezelés arra hivatott, hogy karbantartsa a heapet, és onnan is törlődjön az objektum, mikor a blokk végére értünk
    • Ez nagyon hasonlít a RAII-ra a C++ nyelvben

Tesztkörnyezet

  • https://play.rust-lang.org/
  • Link

Mutability

fn main()
{
    let name = "Thomas";
    name = "Jane";
    println!("Name is: {}", name);
}
error[E0384]: cannot assign twice to immutable variable 
  • Mi a hiba a kódban?

Mutability

  • Rustban változót a \(let\) kulcsszóval definiálunk, és ez Immutable alapból, azaz nem változtatható az értéke
  • Ahhoz, hogy ezen változtassunk ki kell rakni a \(mut\) kulcsszavat
  • Javítsuk ki az előző példát

Mutability

fn main()
{
    let mut name = "Thomas";
    name = "Jane";
    println!("Name is: {}", name);
}

Referenciák

fn main()
{
    let mut name = "Thomas";
    name = "Jane";
    let name_ref = &name;
    println!("Name is: {}", name);
}
  • Mint C/C++ készíthetünk referenciát a változó címére
  • Itt a name_ref immutable, mivel a referencia független a változótól

Referenciák

fn main()
{
    let name = "Thomas"; 
    let name_ref = &mut name;
}
  • Mutable változóra lehet Immutable ref, de fordítva nem
error[E0596]: cannot borrow immutable local variable name as mutable

Struktúrák

#[derive(Debug)]
struct Player
{
    name: String,
    age: u8,
    description: String
}
  •  A Rust nyelv nem feltétlen követi az objektum orientált alapelveket
  • Inkább hasonlít a C struktúrákra amihez definiálhatunk műveleteket

Struktúrák

impl Drop for Player{
    // Destruktor függvény megadása
    fn drop(&mut self) {
        println!("Destrukor lefutott {:?}", self);
    }
}

impl Player{

    fn get_older(&mut self) {
        age+=1;
    }

}

Struktúrák

fn main()
{
    let mut our_player = Player
    {
        name: "Jones".to_string(), //converts &str (static string) to a String (heap memory)
        age: 25,
        description: "Just a happy guy.".to_string()
    };
}

Move

  • Move-nak nevezzük azt mikor átadjuk a tulajdonosi jogot
  • Miután átadtuk azután már az eredeti referencia megszűnik
  • Analógia: mikor eladjuk az autónkat, azután már az új tulajdonos fogja használni, és nekünk nem lesz hozzáférésünk

String példa

  • Mikor megnézzük ezt a kódot azt várjuk, hogy létrehozunk heapen egy \(s1\) stringet, majd \(s2\)-re állítjuk a referenciát
  • Ez nem egészen így történik
let s1 = String::from("hello");
let s2 = s1;

String példa

let s1 = String::from("hello");
let s2 = s1;

String példa

let s1 = String::from("hello");
let s2 = s1;

String példa

let s1 = String::from("hello");
let s2 = s1;

String példa

let s1 = String::from("hello");
let s2 = s1;

String példa

let s1 = String::from("hello");
let s2 = s1;

Függvény példa

  • A függvény átveszi a tulajdonos jogot attól az objektumtól ami bemegy
  • Majd visszatérési értékként visszaadja a tulajdonosi jogot a fogadó félnek
fn mover(moved: Player) -> Player
{
    println!("I am {}, I've been moved into mover", moved.name);
    moved
}

Függvény példa

  • Hol a hiba?
fn main()
{
    let mut our_player = Player
    {
        name: "Jones".to_string(),
        age: 25,
        description: "Just a happy guy.".to_string()
    };
    mover(our_player);
    println!("My name is {}, and I am being used after a move", our_player.name);
}
error[E0382]: use of moved value: our_player.name

Függvény példa - Fix - Immautable Borrow

fn immutable_borrow(borrowed: &Player)
{
    println!("I am {}, I've been immutably borrowed", borrowed.name);
}

fn main()
{
    let mut our_player = Player
    {
        name: "Jones".to_string(),
        age: 25,
        description: "Just a happy guy.".to_string()
    };
    immutable_borrow(&our_player);   
    println!("My name is {}, and I am being used after an immutable borrow", our_player.name);
}

Függvény példa 2

  • Mit a hiba a kódban?
  • Semmi!
  • Mivel az alap típusok implementálják a Copy Trait-et, ezért mikor Move történne lemásolódik az érték
fn main()
{
    let age: u8 = 55;
    mover(age);
    
    println!("The age is: {}", age);
}

fn mover(age: u8) -> u8
{
    println!("Age {} has been moved into mover!", age);
    age
}

Primitív példa

  • Létrehozunk egy \(x\) változót a stacken 5 értékkel
  • Utána létrehozunk még egy \(y\) változót a stacken, az \(x\) értékével, ami 5, és kaptunk két különböző változót, két darab 5-ös van a stacken
let x = 5;
let y = x;

Move - Rebind

  • Mi történik ilyenkor?
  • Van egy Immutable változónk, és azt Move segítségével átadjuk a függvénynek
  • Mivel nem mi vagyunk már a tulajdonos, az lehet már Muable is az új helyen, Rebind történt
#[derive(Debug, Clone)]
struct User {
    a: String,
    b: usize,
}
fn mover_function_with_return(mut user: User) -> User {
    user.b = 456;
    user
}
fn main()
{
    let b = User{ a:String::from("Test2"), b: 234566 };
    mover_function_with_return(b);
}

Borrowing - Immutable

  • Az előző példában már láthattunk példát Immutable borrow-ra
  • Átadunk úgy egy változót, hogy a tulajdonos jogot nem adtuk át, ezért a függvény lefutása után is elérhető lesz az eredeti változó
  • Annyi Immutable Borrow-t készíthetünk, amennyit csak szeretnénk, de csak addig, még a scope nem tartalmaz Mutable Borrow-t
fn immutable_borrow(borrowed: &Player)
{
    println!("I am {}, I've been immutably borrowed", borrowed.name);
}

immutable_borrow(&our_player);   

Borrowing - Mutable

  • Az Immutable borrowing csak olvasható
  • Ha változtatni akarunk akkor Mutable borrow-ra van szükségünk
  • Nézzük meg a szignatúrákat
fn main()
{
    immutable_borrow(&our_player);
    change_name(&mut our_player);
}

fn immutable_borrow(borrowed: &Player)
{
    println!("I am {}, I've been immutably borrowed", borrowed.name);
}

fn change_name(borrowed: &mut Player)
{
    borrowed.name = "My New Name".to_string();
}

Borrowing - Mutable

  • A biztonságos Rust komoly szabályokat követel meg mikor Mutable Borrow történik
  • Egyszerre csak EGY darab Mutable referencia lehet egy változóra, és nem lehet egyetlen Immutable sem
  • Ezzel a Rust megakadályozza azt, hogy verseny helyzet alakuljon ki a változóra
  • Ez persze kikapcsolható, de akkor \(unsafe\) blokkot kell használni

Borrowing - Mutable

  • Mi a hiba a kódban?
  • Rossz compiler opció van beállítva
fn main()
{
    let my_immutable_return = immutable_borrow(&our_player);
    change_name(&mut our_player);
    println!("My name is {}, and I am being used after an mutable borrow", our_player.name);
}

fn immutable_borrow(borrowed: &Player) -> &Player
{
    println!("I am {}, I've been immutably borrowed", borrowed.name);
    borrowed
}

fn change_name(borrowed: &mut Player)
{
    borrowed.name = "My New Name".to_string();
}

Borrowing - Mutable

  • Ez hibát ad, mivel a borrowing az a blokk végéig tart, és ez miatt lesz egy Immutable és Mutable borrow is
fn main()
{
    let my_immutable_return = immutable_borrow(&our_player);
    change_name(&mut our_player);
    println!("My name is {}, and I am being used after an mutable borrow", our_player.name);
}

fn immutable_borrow(borrowed: &Player) -> &Player
{
    println!("I am {}, I've been immutably borrowed", borrowed.name);
    borrowed
}

fn change_name(borrowed: &mut Player)
{
    borrowed.name = "My New Name".to_string();
}

Borrowing - Mutable

  • Ha egy új blokkba rakjuk bele az Immutable Borrow-t akkor az előbb meg fog szűnni, mint mielőtt átadjuk Mutable Borrow-t
fn main()
{
    {
        let my_immutable_return = immutable_borrow(&our_player);
    }
    change_name(&mut our_player);
    println!("My name is {}, and I am being used after an mutable borrow", our_player.name);
}

Miért jó a kód mégis 2018-ban?

Rust memória műveletek- röviden

fn main() {
    // Konstans változó
    let a : i32 = 123;

    // Nem konstans változó
    let mut b : i32 = 987;

    // Objektum létrehozás
    let c = User{ a:String::from("Test2"), b: 234566 };

    // Move
    let d : User = c;

    // Copy, ha implementálja a Copy-t a típus
    let d1 : i32 = a

    // Clone, ha implementálja a Clone-t a típus
    let e : User = c.clone()

    // Immutable Borrow 
    let f : i32 = &c.b;

    // Mutable Borrow
    let g : i32 = &mut c.b;
}

Összefoglalás

  • A Rust nyelv határozottan más gondolkodás módot követel meg a fejlesztőktől
  • A nyelv biztonságossága miatt, nagyon jól használható konkurens programok írására
  • Sebességre felveszi a versenyt a legjobb C++ kódokkal
  • Határozottan jobban kezeli a hibát mint a C/C++
  • Mivel azon ritka nyelvek közé tartozik ami nem rendelkezik GC-vel, a WebAssembly egyik kiemelt nyelve, amivel érdemes arra fejleszteni

Referenciák

Köszönöm a figyelmet

Rusty

By Imre Secodi

Rusty

  • 127