Skip to content

Latest commit

 

History

History
1843 lines (1631 loc) · 44.2 KB

File metadata and controls

1843 lines (1631 loc) · 44.2 KB

Content

Intro

Vorstellung

Wer bin ich?

Timm Behner

  • MSc in Informatik Uni Bonn 2016
  • Fraunhofer FKIE (Cyber Analysis & Defense) bis 2022
    • Datenbanken- & Netzwerk-Forensik
    • Infrastruktur-Projekte
    • Lehre
  • Deutsche Telekom Security
    • Kryptographische Software-Systeme
  • Teig-Monitor mit Arduino, Thermometer, Abstandssensor und Rust

Wer seid ihr?

  • Vorkenntnisse
  • Erwartungen
  • Ziele

Organisation

Zeitplan

TagSlotThema
---------------------------
Sa.09:00 - 10:35Rust Einführung und Speicherverwaltung
Sa.10:45 - 12:15Rust Generics, Traits und Error-Handling
Sa.13:00 - 17:00Übungen
So.09:00 - 10:35Rust OOP und Funktionale Programmierung
So.10:45 - 12:15Rust Fearless Concurrency und Interoperatibilität
So.13:00 - 17:00Übungen

Resourcen

Bug Bounty Program

Belohnung für:

  • gefundene Fehler
  • beantwortete Fragen

Wieso Rust?

Design Goals

  • Reliable: If it compiles, it works
  • Performant: Idiomatic code runs efficiently
  • Suuportive: the language, tools, and community are here to help
  • Productive: a little effort does a lot of work
  • Transparent: you can predict and control low-level details
  • Versatile: You can do anything with Rust

Verbreitung

  • Firefox
  • Amazon (AWS Firecracker und Bottlerocket)
  • Microsoft (Azure – IoT Edge)
  • Cloudflares QUIC und Pingora
  • Linux
  • npm

Beliebtheit

Rust ist die meist geliebte Programmiersprache in der StackOverflow Nutzerstudie

  • seit 7 Jahren
  • seit Rust 1.0
  • mit fast 20%-Punkten Abstand

Perfomance

Anwendungsbereiche

  • Backend Anwendungen (Server-Anwendungen, Microservices, …)
  • System-Werkzeuge
  • Cryptocurrencies, Blockchains, Blockchain-Anwendungen
  • Frontend (Web Assembly)
  • Embedded
  • Programmiersprachen

Rust Einführung

Rust Projekt erstellen

Installation

  • Rustup: Installationsprogramm für Rust und Rust-Komponenten
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh

Installation von src_shell{rustc} und src_shell{cargo}

  • Rust aktuell halten
rustup update
  • Installation von alternativen Toolchains
rustup install nightly
rustup default nightly

Entwicklungsumgebung

  • IDEs
    • IntelliJ Idea (via Plugin)
    • Eclipse (via Plugin)
    • VSCode (with Plugin)
  • Text-Editor und Kommandozeile
    • Emacs
    • Vim
    • VSCode
    • Sublime
    • … jeder mit LSP-Support?
  • Rust Tools Resourcen

Projekte erstellen mit Cargo

  • Erstellen einer neuen Anwendung
cargo new hello_rust
  • Erstellen einer neuen Bibliothek
cargo new --lib greeting

Hello Rust

// src/main.rs
fn main() {
    println!("Hello, world!");
}

Cargo Konfiguration

# Cargo.toml
[package]
name = "hello_rust"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

Greeting Library

// src/lib.rs
pub fn add(left: usize, right: usize) -> usize {
    left + right
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn it_works() {
        let result = add(2, 2);
        assert_eq!(result, 4);
    }
}

Cargo Library Konfiguration

[package]
name = "greeter"
version = "0.1.0"
edition = "2021"

# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html

[dependencies]

Variablen und Datentypen

Variablen deklarieren

  • Deklaration und Initialisierung
    let x: usize = 5;
    println!("x has value {x}");
        
  • Deklaration mit späterer Initialisierung
    let x: usize;
    x = 5;
    println!("x has value {x}");
        

  • Array
    let scores: [u8; 3] = [1, 2, 3];
    println!("Score: {score[0]}");
    println!("Score: {score[1]}");
    println!("Score: {score[2]}");
        
    • gleicher Datentyp
    • feste Kapazität
    • Index aus Ausdruck

Variablen verändern

  • alle Variablen haben einen konstanten (unveränderbaren) Wert
    let x = 5;
    x = 6; //  cannot assign twice to immutable variable `x`
    println!("x has value {x}");
        
  • es sei denn, man macht sie veränderbar
    let mut x = 5;
    x = 6;
    println!("x has value {x}");
        

Datentypen ableiten

  • bei Zuweisung
    let x = 5;
    println!("x has value {x}");
        
  • Kontextabhängig
    let answer = "42".parse(); // Error: type annotations needed
        

Compile-time Konstanten

const PI_FOR_PHYSICS: f64 = 3;
  • zur Kompilierzeit zugewiesen
  • immer src_rust{static}
  • müssen explizit typisiert sein

Häufige Verbunddatentypen auf dem Heap

Vec (src_rust{std::vec::Vec})

let values: Vec<usize> = Vec::from([1, 2, 3, 4]);
let also_values: Vec<usize> = vec![1, 2, 3, 4];
  • variable Anzahl an Elementen
  • gleicher Datentyp
  • Kapazität ≠ Länge
values[0]; // \rightarrow 1
values.len(); // \rightarrow 4
values.capacity(); // \rightarrow 8
values.is_sorted(); // \rightarrow true

mut Vec

Modifizierende Operationen nur auf mutable Vec:

let mut values = vec![1, 2, 3];
values.push(4); // \rightarrow [1, 2, 3, 4]
values.extend(vec![2, 1]); // \rightarrow [1, 2, 3, 4, 2, 1]
values.pop(); // \rightarrow [1, 2, 3, 4, 2]
values.sort(); // \rightarrow [1, 2, 2, 3, 4]
dbg!(values);
  • Erweitern
  • Löschen
  • Sortieren

String (src_rust{std::string::String})

let greeting = String::from("Hello, World!");
dbg!(greeting.len()); // \rightarrow 13
dbg!(greeting.to_lowercase()); // \rightarrow "hello, world!"
dbg!(greeting.as_bytes()); // \rightarrow [72, 101, 108, 108, 111, 44, 32, 87, 111, 114, 108, 100, 33]

  • UTF-8 String
  • variable Länge und Inhalt

mut String

Modifizierende Operationen auf Strings

let mut greeting = String::from("Hello, ");
greeting.push('W');
greeting.push_str("orld!");
dbg!(greeting);

Funktionen

Funktionen definieren und aufrufen

fn say_hello() {
    println!("Hello, World!");
}

fn main() {
    say_hello();
}

Parameter und Rückgabewert

fn greet(greeting: String, subject: String) {
    println!("{greeting}, {subject}!")
}
dbg!(greet("Hello".into(), "World".into()));
fn add_one(x: usize) -> usize {
    return x + 1;
}
dbg!(add_one(41));

Anweisungen und Ausdrücke

  • Anweisungen: Instruktionen die einen Effekt haben, und keinen Wert zurückgeben:
    let x = 5;
        
  • Ausdrücke: werden zu einem Ergebniss ausgewertet und können einen Effekt haben:
    x + 5;
    x;
        

Letzter Ausdruck als Rückgabewerte

Der letzte Ausdruck einer Funktion ist ihr Rückgabewert

fn add_one(x: usize) -> usize {
    x + 1
}

aber auf Semikolon achten:

fn add_one(x: usize) -> usize {
    x + 1;
    // Error - implicitly returns `()` as its body has no tail or `return` expression
}

Kontrollstrukturen (Teil 1)

Bedingte Ausführung

if /*condition*/ {
    /*true case*/
} else {
    /*false case*/
}
  • src_rust{if} sind Ausdrücke
    let y = 10;
    let x = if y == 5 {
        y
    } else {
        y + 1
    };
    dbg!(x);
        

Mehrere Bedingungen

let number = 49;
if number % 2 == 0 {
   println!("{number} is not a prime");
} else if number % 3 == 0 {
   println!("{number} is not a prime");
} else if number % 5 == 0 {
   println!("{number} is not a prime");
} else {
   println!("{number} is probably a prime");
}

Loops

  • Endlos-Schleifen
loop {
    println!("Sind wir bald da?");
}
  • Schleifen sind Ausdrücke
    let arr = [1, 3, 4];
    let mut counter = 0;
    let final_value = loop {
       if arr[counter] % 2 != 0  {
           break arr[counter] * 2
       }
        counter += 1;
    };
    dbg!(final_value);
        

While-Loops

let mut counter = 5;
let mut factorial = 1;
while counter > 1 {
    factorial *= counter;
    counter -= 1;
}
println!("5! = {factorial}")
  • while ist kein Ausdruck, sondern eine Anweisung

For-Loops

Iteration über eine Kollektion zu Iterieren

let a = [1, 2, 3, 4];
for x in a {
    println!("{x}")
}

For-Loop über einen Zahlenbereich

  • exklusives Intervall
for i in 1..4 {
    /* tue etwas mit i*/
}
  • inklusives Intervall
    for i in 1..=4 {
        /* tue etwas mit i*/
    }
        

Tests (Teil 1)

Test Setup

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_name() {
        assert!(true);
    }
}
  • Tests in eigenem Modul
  • Test-Funktionen werden annotiert
  • src_rust{assert} Macros zum Prüfen von Werten
  • src_rust{assert!(false, “This is false!”)} Fehlermeldungen anpassen

Tests ausführen

  • src_shell{cargo test} – alle Tests ausführen
  • src_shell{cargo test – –show-output} – Anzeigen der Standard-Ausgabe
  • src_shell{cargo test test_name} – Ausführen einzelner Tests
  • Ignorieren von einzelnen Tests
    #[test]
    #[ignore]
    fn ignored_test() {
        assert!(false);
    }
        

Tests organisieren

  • Als Sub-Modul (in der gleichen Datei)
  • Als eigenes Modul (gesonderte Datei)
  • Als Integration-Tests in gesondertem Verzeichniss

Speicher, Ownership und Lifetimes

Ownership auf Speicher

Ownership eines Values

{                      // s is not valid here, it’s not yet declared
    let s = "hello";   // s is valid from this point forward
    // do stuff with s
}                      // this scope is now over, and s is no longer valid

Ownership Regeln

  • Each value has exactly one owner
  • When the owner goes out of scope, the value will be dropped

Copy on assignment

{
    let s = "hello";
    let f = s;
    println!("{}, {}!", s, f);
}

Ownership und Funktionen

fn greet(name: String) -> String {
    let mut greeting = String::from("Hello, ");
    greeting.push_str(&name);
    greeting.push('!');
    greeting
}

fn main() {
    let name = "Thorsten".to_string();
    let greeting = greet(name);
    dbg!(greeting);
}

Heap vs Stack

  • src_rust[]{String} auf dem Heap
{
    let s = String::from("hello");
    let f = s;                     // value moved here
    println!("{}, {}!", s, f);      // Error -- value borrowed here after move
}

Referenzen

Konstante Pointer:

{
    let s = String::from("hello");
    let f = &s; // -> &String
    println!("{}, {}!", s, f);
}

Dereferenzieren mit src_rust{*}

let v: usize = 5; // type: usize
let r = &v; // type: &usize
assert_eq!(v, *r);
dbg!(*r);

Veränderbare Referenzen

Referenzen mit Schreibe-Zugriff

{
    let mut s = String::from("hello");
    let f = &mut s;
    f.push_str(" there, General Kenobi!");
    println!("{f}");
}

Alles hat Grenzen!

{
    let mut s = String::from("hello");
    let f = &mut s;
    f.push_str(" there, General Kenobi!");
    println!("{}, {}!", s, f); // Error
    // error[E0502]: cannot borrow `x` as immutable because it is also borrowed as mutable
}

Zu jeder Zeit kann es entweder:

  • eine veränderbare (mutable) Referenz
  • oder eine beliebige Anzahl von nicht-veränderbaren Referenzen geben

Dangling References und Lifetimes

Der Compiler überprüft gültigkeit von Referenzen

fn make_dangling_reference() -> &String {
    let o = String::from("foo");
    &o // return reference to o
} // o goes out of scope
// Error -- expect lifetime parameter
fn make_reference(name: &str) -> &str {
    &name
}

und explizite Lifetimes

fn make_reference<'a>(name: &'a str) -> &'a str {
    &name
}

Slices und Ownership

Slices sind eine Referenz auf einen Teil einer Kollektion

  • String Slices src_rust{&str}
    let s = String::from("Hello, World!");
    
    dbg!(&s[0..5]);
    dbg!(&s[7..12]);
        
  • General Slices src_rust{&[T]}
    let v = vec![1, 2, 3];
    dbg!(&v[1..2]);
        

Explizite Lebenszeit

  • alle bisherigen Refernezen hatten eine implizite Lebenszeit
    fn get_first(input: &str) -> &str {
        &input[0..1]
    }
        
  • explizite Lebenszeit
    fn get_first<'a>(input: &'a str) -> &'a str {
        &input[0..1]
    }
        
  • manchmal notwendig
    fn multiple_lifetimes(first: &str, second: &str) -> &str {
        first
    }
    
    let v = "I'm on the stack";
    {
        let a = String::from("look, I'm on the heap!");
        let result = multiple_lifetimes(v, &a);
        assert_eq!(result, v);
    }
    
        

Statische Lebenszeit

  • Statisch in der erzeugten Binärdatei
  • Gleiche Lebenszeit wie src_rust{main}
const VERSIONNUMBER: &'static str = "v0.0.1"
const DBCONNECTION: &'static Connection = /* ... */

Structs

Definition

  • Gruppieren von zusammenhängenden Daten und Kapselung von Daten
    struct Point {
        x: f64,
        y: f64,
        desc: String,
    }
        
  • Instanzen (Objekte) eines src_rust{struct}
    let origin = Point{x: 0, y: 0, desc: "origin".to_string()}
        
  • Kopieren
    let shifted = Point{x: 1, ..origin}
        
  • Auf Teile (Member) zugreifen mit Punkt-Notation src_rust{origin.desc}

Structs mit Referenzen

  • Structs können Besitzer all ihrer Daten sein
  • können aber auch Referenzen haben
    struct ExtDescPoint<'a> {
       x: f64,
       y: f64,
       desc: &'a str,
    }
        

Methoden

struct Circle {
    radius: f64
}

Methoden für src_rust{struct} definieren

  • Struct-Methoden
    impl Circle {
        fn get_pi() -> f64 {3.141592}
    }
        
  • Objekt-Methoden
    impl Circle {
        fn area(&self) -> f64 {2 * 3.1415 * self.radius * self.radius}
    }
        
  • veränderbare Objekt-Methoden
    impl Circle {
        fn resize(&mut self, new_radius: f64) {self.radius = new_radius}
    }
        

Enums

  • Gruppierung alternativer, fester Werte
    enum DataSource {
        Network,
        FileSystem,
        UserInput,
    }
        
  • jeder Wert kann wiederum Variablen beinhalten
enum DataSource {
    Network(IpAddr),
    FileSystem(std::path::PathBuf),
    UserInput,
}

Smart Pointers

Typen von Smart Pointers

  • src_rust{String}
  • src_rust{Vec}
  • src_rust{Box<T>}
  • src_rust{Rc<T>}
  • src_rust{RefCell<T>} (und src_rust{Ref} / src_rust{RefMut})

Box

Speicher auf dem Heap

  • Rekursive Datentype haben keine bekannte Größe
    enum BinTree {
        Node(BinTree, BinTree),
        Nil,
    }
        
  • Performanz bei Funktionsaufrufen
  • Trait-Objekte (s. OOP)

Rc

Mehrere Owner des gleichen Objektes:

let a = Rc::new(BinTree(Nil, Nil)); // Anzahl der Referenzen 1
let b = BinTree(Rc::clone(a), Nil); // Anzahl der Referenzen 2
{
    let c = BinTree(Rc::clone(a), Nil); // Anzahl der Referenzen 3
}
println!("{Rc::string_count(&a)}")// Anzahl der Referenzen 2
  • src_rust{Rc::clone} erzeugt eine neue Referenz (Pointer)
  • src_rust{a} wird erst gelöscht, wenn es keine Referenzen mehr gibt
  • src_rust{Rc} ist immer eine src_rust{Box}
  • kaum overhead

Schwache Referenzen mit Rc

  • Problem: Zirkuläre Referenzen:
    • doppelt verkettete Listen
    • Referenzen auf Eltern-Knoten in Graphen
  • Erzeugung erhöht nicht den Referenz-Zähler
    let par = Rc::new(5);
    let weak_ref = Rc::downgrade(&par);
        
  • Zugriff kann ins Leere laufen!
    weak_ref.upgrade() match {
        Some(r) => {/* */},
        None => {/* */}
    }
        

RefCell

  • Borrow-Checking-Regeln zur Laufzeit
  • neue RefCell erstellen
    let msg: RefCell<String> = RefCell::new(String::from("hello"));
        
  • Versuche den Wert zu leihen (borrow) → src_rust{Ref}
    {
        if let Ok(m) = msg.try_borrow() {
            // use m here
        }
    } // msg not borrowed anymore
        
  • Versuche den Wert veränderbar zu leihen (mutable borrow) → src_rust{RefMut}
    if let Ok(m) = msg.try_borrow_mut() {
        // change m here
    } // msg not borrowed anymore
        

Rerence Counted Reference Cell

let value = Rc::new(RefCell::new("Shared mutable string"));
  • mehrere Owner von veränderbarem Objekte src_rust{Rc<RefCell<T>>}
  • src_rust{Rc::clone} zum Kopieren der Referenz
  • src_rust{RefMut} zum verändern der Werte

Borrow-Checker-Hell

  • Zuerst muss es kompilieren, kümmere dich um Effizienz später
  • Verschiebe Werte auf den Heap (String, Vec, Box)
  • Kopiere Werte
  • Klone Objekte
  • Verschiebe Objekte in eine src_rust{Rc}
  • Verschiebe Objekte in eine src_rust{RefCell}

In dynamisch typisierten Programmiersprache ist jeder Wert eine src_rust{Rc<RefCell<T>>}!

Traits

Traits definieren und implementieren

  • Gemeinsame Funktionalität die mehrere Structs teilen
    trait Shape {
        fn size(&self) -> f64;
    }
        
  • Structs können Traits implementieren
    struct Circle {
        r: f64
    }
    
    impl Shape for Circle {
        fn size(&self) -> f64 {2 * 3.141529 * r * r}
    }
        

Traits als Parameter

  • src_rust{impl} Schlüsselwort
    fn is_smaller(first: &impl Shape, second: &impl Shape) -> bool {
        first.size() < second.size
    }
        
  • als Trait-Bound (s. Generics)
  • Rückgabewerte mit eindeutigem Typen
    fn get_shape(r: f64) -> impl Shape {
        return Circle{r}
    }
        

Default-Implementierungen

Implementierung von Standardverhalten

impl Named {
    fn get_name(&self) -> String
    fn say_hello(&self) -> String {
        format!("Hello, my name is {}", self.get_name())
    }
}

Derivable Traits

  • Implementierung wird durch Macro erzeugt
#[derive(Debug)]
struct Person {
    name: String
}
let p = Person{name: "Linus"};
println!("{p:?}");
dbg!(p);

Generics

Generische Funktionen

fn find(needle: usize, haystack: Vec<usize>) -> usize {
    //
}

wird zu

fn find<T>(needle: T, haystack: Vec<T>) -> usize {
    //
}

Generische Structs

struct Matrix<T> {
   values: Vec<Vec<T>>
}

impl<T> Matrix<T> {
    fn get(&self, x: usize, y: usize) -> &T {
        &self.values[x][y]
    }
}
  • Container-Datentypen
  • Generische Pattern

Generische Enums

  • src_rust{enums} können auch generische Datentypen haben
    enum Option<T> {
        Some(T),
        None
    }
        
  • Entspricht Algebraischen Datentypen aus funktionalen Programmiersprachen

Trait-Bounds

Anforderungen an generische Typen

struct Matrix<T: Default + Clone + PartialEq> {
   values: Vec<Vec<T>>
}

Beispiel:

  • src_rust{Default}: Standardwert
  • src_rust{Clone}: Wert kann geklont werden
  • src_rust{PartialEq}: Typ ist partiell vergleichbar
fn is_smaller<T: Shape>(first: &T, second: &T) -> bool {
    first.size() < second.size
}

Security

Speicherbasierte Security-Bugs

  • 70% of all security bugs are memory safety issues.
  • Sowohl in Chrome als auch in Microsoft Produkten
  • Zugriffsfehler: Buffer Overflow, Buffer Over-Read, Use After Free, Race Conditions
  • Uninitialisierte Werte: Null pointer dereference, Wild Pointers
  • Speicher Lecks: Double Free, Invalid Free, Unwanted Aliasing

Warum ist Rust sicherer?

  • Keine Null-Pointer
  • Kein uninitialisierter Speicher
  • Kein uninitialisierter Pointer
  • Keine unchecked Memory-Boundaries

Vermiedene Probleme:

  • Buffer Overflows
  • Race Conditions
  • Invalid Page faults
  • Use after free
  • Memory Leaks
  • Dangling-Pointers/Wild-Pointser

Was ist mit dem Rest?

  • DDoS
  • Injections

Error-Handling

KontrollStrukturen (Teil 2)

Matching auf Enums

enum Color {Red, Green, Blue}
let v = Color::Red;
match v {
    Color::Red => println!("this is red")
    Color::Green => println!("this is green")
    _ => println!("this is some other color")
}
  • default-Fall
  • Kompiler überprüft auf Vollständigkeit

Matching auf Enums mit Variablen-Bindung

enum Color {RGB(u8, u8, u8), HEX(String)}
let white = Color::RGB(255, 255, 255);
match white {
    Color::HEX(value) => println!("hex value with #{value}"),
    Color::RGB(r, g, _) => println!("This color is {r} red,  {g} green, and some blue"),
}
  • Bindung an Variablen
  • Ownership-Regeln gelten

ref-Schlüsselwort

  • src_rust{ref} alternative Schreibweise für Referenzen
    let v = 5;
    let r1 = &v;
    let ref r2 = v;
        
  • Referenzen in Matching
    match white {
        HEX(ref value) => println!("{value} is a &str now!"),
        RGB(r, g, _) => println!("This color is {r} red,  {g} green, and some blue"),
    }
        

Match-Pattern

  • Überprüfung von gebundenen Variablen
    let k = Ok(5);
    match k {
        Ok(v) if v % 2 == 0 => println!("even result"),
        Ok(v)  => println!("odd result"),
        Err(_) => println!("some error"),
    }
        
  • Ranges
    let v = 5;
    match v {
        1..=5 => println!("a number between 1 and 5"),
        _ => println!("some different number"),
    }
        
  • Alternativen
    let c = 'q';
    match c {
        'q' | 'Q' => println!("quit"),
        _ => println!("some action"),
    }
        

If-Pattern

Pattern matchting kann auch in if-Verzweigungen genutzt werden.

let k = Ok(5)
if let Ok(n) == k {
    println!("{n}");
}

Result-Enum

Behebbare Fehler repräsentieren

enum Result<T, E> {
    Ok(T),
    Err(E)
}

Fehler behandeln:

use std::fs;
let open_result = fs::File::open("some_file.txt");
let f = match open_result {
    Ok(some_file) => some_file,
    Err(e) => panic!("Could not open file!"),
};

Methoden für Result

let r = Ok(0);
r.is_ok(); // -> true
r.is_err(); // -> false
r.unwrap(); // -> 0
r.unwrap_or_default(5); // -> 0
r.unwrap_or_else(get_some_value()); // -> 0

Fragezeichen-Operator

  • Frühzeitiges terminieren der Funktion mit src_rust{match}
    let file = match std::fs::File::open("some_file.txt") {
        Ok(f) => f,
        Err(e) => return Err(e)
    };
        
  • viel Boilerplate-Code
  • Fragezeichen-Postfix-Operator
    fn read_file(filename: &str) -> Result<String, std::io::Error> {
        let mut file = std::fs::File::open(filename)?;
        let mut out: String = String::new();
        file.read_to_string(&mut out)?;
        Ok(out)
    }
        
  • Rückgabewert muss passen

Nicht-behebbare Fehler

  • src_rust{panic}-Macro zum Beenden des Prozesses
    panic!("that was not okay!");
        
  • src_rust{Result} in src_rust{panic} umwandeln
    let v: Result<String, String> = Ok("Ok...".into());
    let r: Result<String, String> = Err("This is wrong!".into());
    
    let correct_string: String = v.expect("a valid string");
    r.expect("That this blows up!"); // KABOOM!
        

Logische Operationen

  • src_rust{lhs.or(rhs)}
    methodlhsrhsoutput
    ---------------------------
    orOk(x)(ignored)Ok(x)
    orErr(d)Ok(y)Ok(y)
    orErr(d)Err(e)Err(e)
  • src_rust{lhs.and(rhs)}
    methodlhsrhsoutput
    ---------------------------
    andOk(x)Ok(y)Ok(y)
    andOk(x)Err(d)Err(d)
    andErr(d)(ignored)Err(d)

Option-Enum

Optionale Werte

enum Option<T> {
    Some(T),
    None
}

mit Methoden analog / kompatibel zu src_rust{Result}

Methoden für Results und Options

  • Verschachtelte Ergebnisse
let r = Ok(Some("yay!"));
r.transpose(); // -> Some(Ok("yay!"))
  • src_rust{Result} in src_rust{Option} umwandeln
    let maybe = Ok(1);
    maybe.ok() // -> Some(1)
    maybe.err() // -> None
        
  • src_rust{Option} in src_rust{Result} umwandeln
    let maybe = Some(1);
    maybe.ok_or("this should not happen!")
        

Projekte organisieren

Packages, Crates und Module

Nomenklatur

  • Package: Ein oder mehrere Crates
  • Crate: Kleinste Einheit von Code, die der Rust Compiler (und cargo) verarbeitet
    • Library Crates und Binary Crates
    • Beliebig viele Binary Crates
    • Maximal ein Library Crate

Crate Verzeichnissstruktur und Suchheuristik

Module:

  • inline Modul in src_shell{lib.rs}
  • Modul in eigener Datei in src_shell{src}: src_shell{src/submodule.rs}
  • Modul in eigenem Verzeichniss: src_shell{src/submodule/mod.rs}

Sub-Module:

  • inline Sub-Modul in src_shell{module.rs}
  • Sub-Modul in eigener Datei in src_shell{src/module}: src_shell{src/module/submodule.rs}
  • Sub-Modul in eigenem Verzeichniss: src_shell{src/module/submodule/mod.rs}

Sichtbarkeit

  • Jedes Modul ist zunächst privat
  • src_rust{pub mod} für öffentliche Module
  • Sub-Module können auf alle Funktionen, Structs, etc. von Eltern-Modulen zugreifen

Funktionalität importieren

  • externe Crates src_rust{use regex;}
  • absoluter Pfad src_rust{use crate::module::submodule;}
  • relativer Pfad
    • src_rust{use self::module::submodule;}
    • src_rust{use super::othermodule::submodule;}
    • src_rust{use submodule::subsubmodule;}
  • Aliase erzeugen src_rust{use regex as re; }
  • Re-Exportieren src_rust{pub use crate::module::submodule;}

Abhängigkeiten von externen crates

  • Von der Kommandozeile src_shell{cargo add regex}
  • Abhängigkeiten von Crates auf crates.io
    # Cargo.toml
    [dependencies]
    regex = "1.7.0"
        
  • Abhängigkeiten von GitHub
    [dependencies]
    regex = { git = "https://github.com/rust-lang/regex" }
        
  • optionale Features
    [dependencies]
    # Enables the `derive` feature of serde.
    serde = { version = "1.0.118", features = ["derive"] }
        

Weitere ausführbare Anwendungen

  • src_shell{mypkg/src/main.rs} ausführen: src_shell{cargo run –bin mypkg}
  • src_shell{mypkg/src/bin/other.rs} ausführen: src_shell{cargo run –bin other}
  • Taget-Konfiguration
    # Cargo.toml
    [[bin]]
    name = "newname"
    path = "bin/other.rs"
    # ...
        

OOP

Ist Rust objektorientiert?

  • Typen, Kapselung von Daten und Methoden: YES!
  • Polymorphismus (Trait-Bounds, Generics): YES!
  • statisches und dynamisches Dispatching: YES!
  • Subtyping (auf src_rust{struct}s) und Vererbung: NOPE…
  • Rust Book zu OOP

Trait Objects

  • generische Objekte die Trait implementieren
  • dynamical Dispatch: konkrete Implementierung wird zur Laufzeit ermittelt
  • Unbekannte Größe zur Compile-Time
let v: Box<dyn MyTrait> = Box::new(MyStruct{})

Default implementations

Implementierung von Standardverhalten

impl Named {
    fn get_name(&self) -> String
    fn say_hello(&self) -> String {
        format!("Hello, my name is {}", self.get_name())
    }
}

Super-Traits

  • Traits die auf Funktionalität anderer Traits beruhen
    trait Shouted: fmt::Display {
        fn shouted(&self) {
            let out = format!("{self}"); // self needs fmt::Display
            println!("{}!!!!", out.to_ascii_uppercase());
        }
    }
        
  • Traits kombinieren
    trait ShoutedInfo: fmt::Display + fmt::Debug {
        fn shouted(&self) {
            let out = format!("{self} ({self:?})"); // self needs fmt::Display
            println!("{}!!!!", out.to_ascii_uppercase());
        }
    }
        

Häufige Patterns

Build Pattern

  • Erzeugung in mehreren Schritten
  • mutable während der Erzeugung
  • imutable finales Objekt
let mut builder = GreeterBuilder::new();
let greeting = builder.set_name("Rust")
 .set_greeting("Hello")
 .build()

Funktionale Programmierung

funktionale Programmierung als Paradigma

  • nicht veränderbare Werte
  • pure Funktionen
  • Komposition von Funktionen (First Class Functions)

First Class Functions

Funktionen als Argumente übergeben

  • Funktionen übergeben
    fn say_something() {
        println("something");
    }
    
    fn do_twice(f: fn() -> ()) -> () {
        f();
        f();
    }
    
    do_twice(say_something);
        
  • Funktionen mit Argumenten und Rückgabewert
    fn twice_the_size(f: fn(&str) -> i32, s: &str) -> i32 { f(s) + f(s) }
        

Funktionen als Rückgabewert

Funktionen können Rückgabewert einer Funktion sein

pub fn get_operator(desc: &str) -> fn(usize, usize) -> usize {
    match desc {
        "+" => add,
        "*" => mult,
        _ => panic!("unkown"),
    }
}

Closures

Closures definieren

  • Closures sind anonyme Funktionen die ihre Umgebung “einfangen”.
  • Closures sind repräsentiert durch Trait src_rust{Fn} oder src_rust{FnMut}
  • keine bekannte Größe zur Compile-Time
  • Closures sind keine Funktionen (aber funktionen implementieren src_rust{Fn})
  • Typen können deduziert werden
fn  add_one_v1   (x: u32) -> u32 { x + 1 }
let add_one_v2 = |x: u32| -> u32 { x + 1 };
let add_one_v3 = |x|             { x + 1 };
let add_one_v4 = |x|               x + 1  ;

dbg!(add_one_v4(1 as u32)); // x: u32 und return-Wert u32

Closures fangen ihre Umgebung ein

  • als nicht veränderbarer Wert
    {
        let y: usize = 5;
        // add_some: Fn(usize) -> usize
        let add_some = |x: usize| -> usize {x + y};
        add_some(5); // -> 10
    }
        
  • als veränderbarer Wert
    {
        let mut y: usize = 0;
        // add_nothing: FnMut(usize) -> usize
        let add_nothing = |x: usize| -> usize {y += 1; x};
        add_some(5); // -> 5 und y == 1
    }
        

Closures und Ownership

  • Referenz auf Wert aus Umgebung
    let x = vec![1, 2, 3];
    let first = || {x.len()};
    dbg!(first());
        
  • Mutable Referenz auf Wert aus Umgebung
    let mut x = vec![1, 2, 3];
    let mut second = || {x.push(4)}; // second: FnMut, x: &mut Vec
    second();
    x.len(); // 4
        
  • Besitz übernehmen
    let mut x = vec![1, 2, 3];
    let third = move || {x.push(4); x.len()};
    third(); // 4
    x.len(); // Error
        

Iterators

Maps

  • Iterieren über Collections
    (1..3).map(|i| i*2).for_each(|x| println!("{x}"));
        
  • und einsammeln
    let even_numbers: Vec<usize> = (1..3).map(|x| x * 2)
        .collect(); // [2, 4, 6]
    dbg!(even_numbers);
        
  • auf src_rust{HashMap}
    use std::collections::HashMap;
    let even_numbers: HashMap<usize, usize> = (1..3).map(|x| (x - 1, x * 2))
        .collect(); // {0: 2, 1: 4, 2: 6}
    dbg!(even_numbers);
        

Filter

  • Werte auf Eigenschaften prüfen
    (1..100).filter(|x| x % 2 == 0)
        
  • Filter mit Umgebung
    (1..100).filter(|x| cum += x; cum < 25)
        

Folds

  • Akkumulieren von Ergebnissen
    (1..100).fold(0, |acc, x| acc + x)
        

    $(\ldots((0 + 1) + 2) + 3) \ldots + 100)$

  • src_rust{rfold} Akkumulieren von Rechts
    (1..100).rfold(0, |acc, x| acc + x)
        

Results und Options

  • src_rust{Results} und src_rust{Options} haben auch src_rust{map}
    Ok(2).map(|x| x * 2); // -> Ok(4)
    Err("foo").map(|x| x * 2); // -> Err("foo")
    Some("foo").map(|s| s.len() ) // -> Some(3)
        
  • boolsche Operatoren mit Closures src_rust{Fn(T) -> Result<U,E>}
    Ok(5).and_then(|x| Ok(x * 2)) // -> Ok(10)
    Err("some error").or_else(|error| error_recovery(error))
        

Fearless Concurency

Concurrency und Parallelism

Definitionen

  • Paralelität: zeitgleiche Ausführung von mehreren Vorgängen (OS-Threads und Prozesse)
  • Nebenläufigkeit: gemeinsame Ausführung von mehreren Vorgängen
    • Präemptive Nebenläufigkeit: ausführender Vorgang wird von Management-Prozess unterbrochen (greenlets, goroutines)
    • Kooperative Nebenläufigkeit: ausführender Vorgang pausiert selbständig und lässt anderen Vorgang weiterarbeiten (JavaScript async/await, Python asyncio, Rust async-await)

Probleme mit Speicher

  • Deadlocks: simultanes Warten auf Resourcen
  • Race-Conditions: simultaner ändernder Zugriff auf gleichen Speicher

Threads

Threads starten und beenden

  • starten eins Threads
    thread::spawn(|| { println("Hello, Threads!"); });
        
  • warten auf Ende eines Threads
    let handle = thread::spawn(|| {
        println("Give me a minute");
        thread::sleep(Duration::from_secs(60));
        println("Ok, done!");
    });
    handle.join.unwrap(); // wartet auf thread
        
  • src_rust{handle.is_finished()} prüft ob der Thread fertig ist

Ownership und Threads

fn main() {
    let name = String::from("Rust");
    thread::spawn(move || {println("Hello, {name}");})
        .join().unwrap();
}
  • Laufzeit von Threads kann nicht geprüft werden
  • src_rust{move}, src_rust{Rc} oder src_rust{Clone}

geteilten Speicher synchronisieren

  • src_rust{Rc} → src_rust{Arc}
  • src_rust{RefCell} → src_rust{Mutex}
  • src_rust{RwLock}: multiple readers, single writer

Shared State mit Mutexen

let cnt = Arc::new(Mutex::new(0));

let cnt_clone = Arc::clone(&cnt);
thread::spawn(move || {
    let mut local_cnt = cnt_clone.lock().unwrap();
    local_cnt += 1;
});

Speicher teilen über Channels

Do not communicate by sharing memory; instead, share memory by communicating.

(Go Dokumentation)

  • Mechanismus zum Übertragen von Werten und Objekten
  • src_rust{mspc::channel}

Channel erstellen und Nachrichten übertragen

let (transmitter, receiver) = mscp::channel();

thread::spawn(|| { transmitter.send(String::from("foo")).unwrap()} );
let msg = receiver.recv().unwrap();
  • mit src_rust{transmitter} beliebig viele Nachrichten senden
  • src_rust{recv} ist ein src_rust{Err} sobald src_rust{transmitter} gelöscht ist

mpsc: Multiple Producers, Single Consumer

let (transmitter, receiver) = mscp::channel();

let t1 = transmitter.clone();
thread::spawn(|| { t1.send(String::from("foo")).unwrap()} );
let t2 = transmitter.clone();
thread::spawn(|| { t2.send(String::from("bar")).unwrap()} );

drop(transmitter);

for msg in receiver {
    println!("{msg}");
}

Sync und Send Traits

  • Send-Trait: Objekte können zwischen Threads transferiert werden
  • Sync-Trait: src_rust{T} implementiert src_rust{Sync}, wenn src_rust{&T} src_rust{Send} implementiert
  • alle primitiven Typen
  • nicht src_rust{Rc}
  • alle Typen die aus src_rust{Send} / src_rust{Sync} zusammengesetzt sind

Interoperatibilität

Interoperation mit C++

Anwendungsfälle

  • (alte) C++-Bibliothek von Rust aus benutzen
    • legacy Bibliotheken weiter benutzen
    • legacy Entwicklung in Rust fortführen
  • Rust-Bibliothek implementieren und von C++ aus benutzen
    • C++-Anwendung in Rust weiterentwickeln
    • Rewrite in Rust schrittweise durchführen

Pakete

CXX

  • “Rust ← C++”-Build-System mit cargo
  • Boilerplate-Code wird automatisch generiert
  • Übersetzen von Typen
  • Teilen von Daten-Strukturen

CXX Bridge: Rust -> C++

  • C++ Funktionen deklarieren
    #[cxx::bridge]
    mod ffi {
        unsafe extern "C++" {
            include!("package/include/header.h");
    
            fn get_string_len(input: &str) -> usize;
        }
    }
        
  • Aufruf in Rust
    let str_size = ffi::get_string_len("Hello, World!");
        
  • C++ Implementierung
    // strlen.h
    uint64_t get_string_len(rust::Str);
        
    uint64_t get_string_len(rust::Str input){
      return input.size();
    }
        

CXX Bridge: C++ -> Rust

  • Rust-Funktionen deklarieren
    #[cxx::bridge(namespace = "org::strlib")]
    mod ffi {
        extern "Rust" {
            fn get_size(input: &CxxString) -> usize;
        }
    
    }
        
  • Rust-Funktion in C++ benutzen
    auto len = org::strlib::get_size("Hello, World!")<< std::endl;
        
  • Rust-Funktion implementieren
    fn get_size(input: &CxxString) -> usize {
        input.len()
    }
        

CXX Type-Übersetzung

  • rudimentäre Typen (src_cpp{uint64_t} → src_rust{u64})
  • Rust-Typen in C++ übersetzt
    RustC++
    ------------------------------
    Stringrust::String
    &strrust::Str
    Vec<T>rust::Vec<T>
    Result<T>throw/catch
  • C++-Typen in Rust übersetzen
    RustC++
    ------------------------------
    CxxStringstd::string
    UniquePtr<T>std::unique_ptr<T>
    CxxVector<T>std::Vec<T>

Tour durch Projekte

Rust interoperation mit Python

Anwendungsfälle

  • Bottlenecks in Python-Anwendungen in Rust implementieren Beispiele:
  • Rust-Anwendungen mit Python automatisieren

Python-Pakete in Rust

  • PyO3
    • Rust ↔ Python
    • Type-Konvertierung
  • Maturin
    • Initialisieren
    • Bauen
    • Verteilen (außer manylinux)

maturin

  • Initialisieren von neuen Projekten
    maturin init
        
  • Bauen von Python-Modulen/Paketen
    maturin build
        
  • Installieren von Python-Paket in Virtualenv
    maturin develop
        

Python-Modul in Rust mit PyO3

#[pymodule]
fn pyrsdemo(_py: Python, m: &PyModule) -> PyResult<()> {
    m.add_function(wrap_pyfunction!(get_file_size, m)?)?;
    Ok(())
}

Python-Funktionen in Rust

#[pyfunction]
fn get_file_size(a: &str) -> PyResult<u64> {
    let path = PathBuf::from(a);
    if !path.exists() {
        return Err(PyValueError::new_err("{a} does not exist!"));
    }

    Ok(path.metadata()?.len())
}
  • PyResult für Python-Errors
  • Transparente Type-Konvertierung

Python-Klassen in Rust

  • Struct → Python-Class
    #[pyclass]
    struct MyGreeter{
        greeting: String,
    }
        
  • Methoden und Dunder-Methoden definieren
    #[pymethods]
    impl MyGreeter {
        #[new]
        fn __new__(greeting: &str) -> Self {
            MyGreeter{ greeting: greeting.to_string() }
        }
    
        fn greet(&self, subj: &str) -> String {
            format!("{self.greeting}, {subj}!")
        }
    }
        
  • Vererbung

Python von Rust aufrufen

  • Initialisieren von Python und des GILs
    pyo3::prepare_freethreaded_python();
        
  • GIL reservieren
    Python::with_gil(|py| {
        })
        
  • Python-Code ausführen
    let result: Result<Option<Vec<u64>>, PyErr> = py
            .eval("[i * 10 for i in range(5)]", None, None)?
            .extract()?;