Einführung
Webseiten
Videos
Rust, for people who know some C++
Blogs
Systemnahe Programmierung
Bei systemnaher Programmierung muss sich der Programmierer direkt mit den Ressourcen und Funktionen des zugrunde liegenden Betriebssystems oder der Hardware auseinandersetzen.
Im Gegensatz zur anwendungsnahen Programmierung, bei der der Programmierer sich auf die Verwendung von Bibliotheken und APIs konzentriert.
Direkter Zugriff auf:
- Hardware
- Speicher
- Systemcalls
Typische Aufgaben:
- Betriebssystemfunktionen
- Prozessverwaltung
- Speicherverwaltung
- Dateisystemverwaltung
- Entwicklung von Treibern
- Systemdienste
- Netzwerkdienste
- Datenbankdienste
C
-----------------
< Systemnaehe? C! >
-----------------
\
\ .~-~.
/^ ^\
(__ O O __)
\ \< >/ /
The good
- Standard seit 70er
- Portabel
- Low-Level
- Manual Memory Management
- Legacy Code
the bad
- Undefined Behaviour
- Keine Memory Safety Garantie
- Fehleranfaellige Parallelverarbeitung
- Error handling
- Dynamisches Linking
and the ugly
- Keine einheitliche Paketverwaltung
- Mehrere Build-Systeme
Undefined Behaviour
In C ist Undefined Behaviour (UB) nicht immer offensichtlich.
Anhand des unsafe
Keywords kann man in Rust sofort Codestellen erkennen, an
denen Undefined Behaviour auftreten kann.
Keine Scheu vor unsafe
!
C - Compiler Unterschiede
int main() { // 1
int i = 2; // 2 Initialize variable
int result = --i + i++; // 3
return 0; // 4
}
gcc: result=3 clang: result=2
C - Buffer Overflow
- Buffer mit bestimmter Groesse erstellen
- Daten in Buffer schreiben
- Daten sind groesser als Buffer
- Naheligender Speicher wird ueberschrieben -> Overflow
#include <stdio.h>
#include <string.h>
int main() {
char buffer[10];
char *src = "This is a long string.";
memcpy(buffer, src, strlen(src));
printf("%s\n", buffer);
return 0;
}
C - Use after free
- Allokieren von Speicher
- Schreiben in den Speicher
- Freigeben des Speichers
- Lesen aus dem Speicher -> Fehlerhaft
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int)); // TODO check allocation success
*ptr = 42;
printf("ptr: %d\n", *ptr);
free(ptr);
// Pointer is dangling
printf("ptr after free: %d\n", *ptr);
return 0;
}
C - Double Free
- Speicher wird allokiert
- Pointer wird an free uebergeben
- Speicher is freigegeben, Pointer existiert weiter
- Pointer wird erneut an free uebergeben -> Welcher Speicher wird bereinigt?
#include <stdio.h>
#include <stdlib.h>
int main() {
int *ptr = (int *)malloc(sizeof(int));
free(ptr);
free(ptr);
return 0;
}
C - Invalid Index
- Index ist groesser als array
- Value enthaelt falsche Daten
#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int value = arr[6];
printf("Value: %d\n", value);
return 0;
}
C - Divide by Zero
- Undefinded
- Manche Programme brechen ab, manche arbeiten mit falschem Ergebnis weiter
#include <stdio.h>
int main() {
int numerator = 10;
int denominator = 0;
int result = numerator / denominator;
printf("Result: %d\n", result);
return 0;
}
C - Dangling Pointer
- Value in einem lokalen Scope
- Dereferenzieren des Values im lokalen Scope
- Referenz (Pointer) wird returned
- Value existiert nicht mehr, Pointer schon
- Pointer zeig auf fehlerhaften Speicher
int *getDanglingPointer() {
int value = 42;
return &value;
}
int main() {
int *ptr = getDanglingPointer();
*ptr = 10; // Unsicher welcher Speicher beschrieben wird
printf("%d", *ptr);
return 0;
}
Rust
Rust ist keine esoterische Programmiersprache.
Denn Rust loest folgende Probleme:
- Ownership
- Memory Safety
- Unsafe
- Statisches Linking
- Cargo
- Paketverwaltung
- Build System
- Package Registry crates.io
- Parallelverarbeitung
- Explicit Error handling
------------
< Yay, Rust! >
------------
\
\ .~-~.
/^ ^\
(__ O O __)
\ \< >/ /
Typsysteme
statisch
- Kompilierung
dynamisch
- Laufzeit
hybride
- Jit, etc.
Typsysteme sind ein wichtiger Bestandteil von Programmiersprachen. Sie helfen dabei, Programme korrekter und sicherer zu machen.
ADTs
In der Praxis ermöglichen algebraische Datentypen eine klare und strukturierte Modellierung von Daten, was besonders in funktionalen Programmiersprachen wie Haskell, ML oder Scala häufig genutzt wird. Sie bieten eine elegante Möglichkeit, komplexe Datenstrukturen zu definieren und zu manipulieren.
Summen Typ
Ein Summentyp repräsentiert eine Menge von möglichen Werten.
Beispiel:
Option = Some(bool) | None
Hier bedeutet Some(bool)
, dass dieser Typ von einem Boolean begleited wird.
None hat keinen Wert, sondern steht nur als Typ fuer sich.
Dieser Typ kann im Ast Some 2 Werte einnehmen |{true, false}|. Im Ast None genau einen |{None}|. Daraus ergibt sich die Summe 3.
Produkt Typ
Ein Produkttyp repräsentiert eine Struktur, bei der alle enthaltenen Werte gleichzeitig vorhanden sind
Beispiel:
Punkt = x: u8, y: u8
Hier ist Punkt ein Produkttyp mit den Feldern x und y, die beide benötigt werden, um einen Punkt vollständig zu definieren.
Ideen
Sammlungen
Creative Projects for Rust Programmers
Practical Rust Projects: Building Game, Physical Computing, and Machine Learning Applications Springer oder O'Reilly
Anregungen
Shing Lyu, Practical Rust Web Projects: Building Cloud and Web-Based Applications, Apress 2021.
Carlo Milanesi, Creative Projects for Rust Programmers, Packt Publishing 2020.
Ken Youens-Clark, Command-Line Rust, O'Reilly 2022.
Python Module in Rust erstellen
Dateiverschlüsselung, von Go nach Rust übertragen
Tooling
Rustup
rustup
ist ein Tool um die Installation und Verwaltung von "Rust-Toolchains" zu erleichtern.
Diese Toolchains sind Compiler fuer verschiedene Betriebssysteme und Architekturen.
Mit rustup
kann man mehrere dieser Compiler installieren, verwalten und updaten.
rust-analyzer
rust-analyzer
ist ein Language Server fuer Rust.
Dieser bietet Funktionen zur statischen Code-Analyse, Autovervollständigung, Refactoring, Fehlererkennung und andere hilfreiche Features.
Cargo
In Rust ist Cargo sowohl ein Build-System als auch ein Paketmanager, was bedeutet, dass es sich um den gesamten Build-Prozess kümmert und gleichzeitig die Verwaltung von Abhängigkeiten erleichtert. Dies trägt dazu bei, dass Rust-Projekte leichter zu erstellen, zu teilen und zu warten sind.
- Build-System
- Kompilierung von Software
- Definiert, wie Quellcode in ausführbaren Code umgewandelt wird (Kompilierungsoptionen)
- Dependencies
- Paketmanager
- Verwaltung von Abhängigkeiten und Bibliotheken in einem Softwareprojekt
- Herunterladen, Installieren und Aktualisieren von Bibliotheken oder Modulen
- Richtige Versionen von Abhängigkeiten
Clippy
clippy
ist ein Linter, der dazu dient, Quellcode statisch zu analysieren.
- Identifiziert syntaktische Fehler oder logische Unstimmigkeiten im Code
- Ueberprüft den Code auf die Einhaltung von Coding-Standards oder Stilrichtlinien
- Weist auf bewährte Methoden und Muster hin, um die Qualität und Lesbarkeit des Codes zu verbessern
Empfohlene Einstellungen:
cargo clippy --fix -- \
-W clippy::pedantic \
-W clippy::nursery \
-W clippy::unwrap_used \
-W clippy::expect_used
rustfmt
rustfmt
ist ein Formatter, der den Quellcode eines Programms automatisch neu formatiert.
- Einhaltung von Stilrichtlinien und Konventionen
- Lesbarerer Code
- Konsistenter Code-Stil
Rustdoc
rustdoc
generiert automatisch Dokumentation aus Doc-Comments (diese werden mit ///
geschrieben).
- Einheitliche Dokumentation
- Dokumentierter Code
- Keine Dokumentations Redundanz
- Automatische Dokumentations Webseite in einheitlichem Stil
Weitere Informationen im Rustdoc Book.
Bacon
Bacon ist ein Interaktiver rust code checker.
CLI
Warum CLI
- Flexibel
- Transparent
Principles
- Benutzerfreundlichkeit
- Human readable
- Verwendung nicht nur von anderen Programmen
- Einfache Bausteine
- Kombinieren von kleinen, einfachen Programmen zu groesserem System
- Pipes und Filterarchitektur
- Kombinierbar durch stdin/stdout/stderr, signals, exit-codes
- Konsistenz über Programme hinweg
- Bereits bestehenden Mustern folgen
- Intuitiv
- Robustheit
- Falscher input muss korrekt behandelt werden
Guidelines
- Use a command-line argument parsing library where you can
- Return zero exit code on success, non-zero on failure.
- Send output to stdout
- Send messaging to stderr
- Display help text when passed no options, the -h flag, or the --help flag
- Display a concise help text by default
- A description of what your program does
- One or two example invocations.
- Descriptions of flags
- Display help for Subcommands (like
git commit --help
) - In help text, link to the web version of the documentation
- Lead with examples
Output
- Human-readable output is paramount
- Have machine-readable output where it does not impact usability
- If human-readable output breaks machine-readable output, use --plain to display output in plain, tabular text format for integration with tools like grep or awk
- If you change state, tell the user.
- Make it easy to see the current state of the system
- Use color with intention
- Disable color if your program is not in a terminal or the user requested it
Errors
Arguments and Flags
Subcommands
Configuration and Environment Variables
Anyhow
Chrono
Clap
Installation: cargo add clap --features derive
- Derive Parser from
clap::Parser
#![allow(unused)] fn main() { #[derive(Parser)] struct Cli {} }
- Configure the Parser
#![allow(unused)] fn main() { #[derive(Parser)] #[command( name = "Cli", author = "John Doe <john.doe@example.com", version = "1.0", about = "Does awesome things", long_about = None, )] struct Cli {} }
Or read values from Cargo.toml:
#![allow(unused)] fn main() { #[derive(Parser)] #[command(author, version, about, long_about = None)] struct Cli {} }
- Change Parser behaviour
Change the help text. A list of possible placeholders can be found here:
#![allow(unused)] fn main() { #[command(help_template = "{name} ({version}) - {usage}")] }
Disable the help flag (-h and --help):
#![allow(unused)] fn main() { #[command(disable_help_flag = true)] }
Require each argument to have a help text:
#![allow(unused)] fn main() { #[command(help_expected = true)] }
- Add arguments
- Positional
#![allow(unused)] fn main() { #[derive(Parser)] struct Cli { /// This is a positional, required argument positional: String, /// This is an optinal argument positional: Optional<String>, // Variadic positional arguments name: Vec<String>, } }
- Flag
#![allow(unused)] fn main() { #[derive(Parser)] struct Cli { /// On/Off switch #[arg(short, long)] release: bool, /// Set the default value to true #[arg(short, long, action = clap::ArgAction::SetFalse)] quiet: bool, /// Flag that counts how often it is set, eg. -vvv would be 3, -vv would be 2 #[arg(short, long, action = clap::ArgAction::Count)] verbose: u8 } }
- Subcommand
#![allow(unused)] fn main() { }
- Default
- Validate arguments
- Enumerated Values
- Validated Values
- Argument Relations
- Custom Validation
Arg Actions
Set Append SetTrue SetFalse Count Help HelpShort HelpLong Version
Daemonize
- Hintergrundprozess
Init-System
- PID1 systemd, runit, openrc
Serde
Beginners Cheatsheet
Traits
Display
Wie lassen sich Structs als String darstellen?
- Python
def __str__()
- Java
public String toString()
- Rust
trait Display
#![allow(unused)] fn main() { pub trait Display { fn fmt(&self, f: &mut Formatter<'_>) -> Result<(), Error>; } }
Beispiel:
#![allow(unused)] fn main() { struct ID(u32); impl Display for ID { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.0) } } #[test] fn test_display() { let id = ID(123); let s = format!("{id}"); assert_eq!(s, "ID(123)"); assert_eq!(id.to_string(), "ID(123)"); } }
From & Into
Wie lassen sich Typen value-to-value konvertieren?
- Java
constructor overloading
- Rust
trait From
#![allow(unused)] fn main() { pub trait From<T>: Sized { fn from(value: T) -> Self; } }
Beispiel:
#![allow(unused)] fn main() { struct ID(u32); impl From<u16> for ID { fn from(value: u16) -> Self { Self(value as u32) } } #[test] fn test_from() { let id = ID::from(123); assert_eq!(id.0, ID(123).0); let id: ID = 123.into(); assert_eq!(id.0, ID(123).0); } }
From
implementiert auch den Into
-Trait mit, dieser ermoeglicht den .into()
-Call
From
darf nicht fehlschlagen, dafuer existiert der TryFrom
-Trait,
z. B. kann man diesen gut fuer Konvertierung von Strings in eigene Typen verwenden,
um bei Fehlerhaften Strings ein Result zurueck zu geben.
Deref
#![allow(unused)] fn main() { pub trait Deref { // Ergebnis-Typ nach dereferenzierung type Target: ?Sized; fn deref(&self) -> &Self::Target; } #[test] fn test_deref() { let id = ID(123); assert_eq!(123, *id); } }
Beispiel:
#![allow(unused)] fn main() { struct ID(u32); impl<T> Deref for DerefExample<T> { type Target = T; fn deref(&self) -> &Self::Target { &self.value } } }
Default
Wie kann man einem Typen default Werte geben?
- Python
__init__(self, id=123, username="foo")
- Rust
trait Default
#![allow(unused)] fn main() { pub trait Default: Sized { // Required method fn default() -> Self; } }
Beispiel:
#![allow(unused)] fn main() { struct ID(u32); impl Default for ID { fn default() -> Self { Self(0) } } }
Beispiel mit Enum:
#![allow(unused)] fn main() { enum Command { Help, Run, Build, } impl Default for Command { fn default() -> Self { Command::Help } } }
Iterator
Wie mache ich eigene Typen iterierbar?
- Python
def __next__(self)
- Java
implements Iterable<T>
- Rust
trait Iterator
Beispiel
#![allow(unused)] fn main() { struct IDS(Vec<u32>); impl Iterator for IDS { type Item = u32; fn next(&mut self) -> Option<Self::Item> { self.0.pop() } } #[test] fn test_iterator() { let mut ids = IDS(vec![1, 2, 3]); assert_eq!(ids.next(), Some(1)); assert_eq!(ids.next(), Some(2)); assert_eq!(ids.next(), Some(3)); } }