Notizen zu Rust.
Rust – die Sprache, der Compiler rustc, sowie die Standardbibliothek – werden ständig weiterentwickelt. In Rust geschriebener Code soll dabei stets mit zukünftigen Versionen von Rust fehlerfrei kompilieren. Brüche erfolgen nur innerhalb einer jeweils neuen, vom Entwickler auswählbaren Edition.
Eigenschaften vergangener und zukünftiger Rust-Versionen werden auf der Webseite releases.rs aufgelistet.
Ein Ziel von Rust sind „Zero-Overhead Abstractions“. Damit sind Abstraktionen gemeint, die ohne vermeidbare Laufzeitkosten auskommen. Das bedeutet:
Gelegentlich wird dies auch mit „Zero-Cost“ bezeichnet, was aber zu dem Missverständnis führen kann, dass durch die Abstraktionen überhaupt keine Laufzeitkosten entstehen.
Beispiele für Zero-Overhead Abstraktionen in Rust:
Es gibt keine unvermeidbare Laufzeitumgebungen für:
Dass Rust nur eine minimale Standard Library anbietet, war eine bewusste Entscheidung. Cargo ermöglicht es aber, eigene Libraries und Libraries anderer Entwickler („Crates“) effizient zu nutzen.
Chancen:
Risiken:
Beispiele für nicht optimalen Code in Standard Libraries:
C:
rand(): TODOGo:
net/http: TODO Projekte wie grpc-go verwenden eigene HTTP/2-Implementierungen.Java:
java.util.Calendar, java.util.Date) war bis Java 8 jahrelang in einem unbefriedigenden Zustand.PHP:
Python (siehe auch PEP 594):
asyncio: TODO curio, triohttp.server: TODOsubprocess: TODOurllib, urllib2: TODORust:
std::sync::mpsc: die API hat sich über die Jahre als nicht optimal herausgestellt. Die Implementierung wurde durch das unabhängig entwickelte crossbeam-channel ersetzt.Beispiele verbreiteter Libraries, die nicht Teil einer Standard Library sind:
C++:
boostJava:
log4jSpringPython:
DjangoFlaskRequestsRust:
lazy_static vs once-cellrustc_serialize vs SerdeDatentypen können mittels Traits gruppiert werden. Traits können Methoden sowohl deklarieren, als auch default-implementieren. Daten-Member können in Traits jedoch nicht definiert werden.
Traits werden für einen Datentyp implementiert, entweder:
derive-Attributsimpl Trait for Datentyp { ... }Trait-Deklarationen und -Implementierungen können sich in unterschiedlichen Crates befinden. Die Orphan Rule verbietet jedoch, dass ein Trait aus einem externen Crate für einen Datentyp aus einem externen Crate implementiert wird. Dadurch wird sichergestellt, dass es für jede Kombination aus Datentyp und Trait unmissverständlich nur eine einzige Implementierung gibt.
Auto Traits werden mit dem Keyword auto markiert. Falls er es für angebracht hält, implementiert der Compiler diese Traits für Datentypen automatisch. Beispiele sind std::marker::Send und std::marker::Sync.
TODO : Blanket Implementations
Um eine Trait-Methode für einen Datentyp aufrufen zu können, muss neben dem Datentyp auch der Trait importiert werden.
Methoden können auch als freistehende Funktion aufgerufen werden. Ist z.B. die Methode A::f(&self, b: B) implementiert, so kann sie für eine Instanz a von A nicht nur als a.f(b), sondern auch als A::f(a, b) verwendet werden. Freistehende Funktionen können allerdings nicht als Methoden aufgerufen werden.
Von statischem Polymorphismus spricht man, falls Trait-Methoden aufgerufen werden und der Datentyp zur Kompilierzeit bekannt ist: impl Trait.
Einschränkungen:
impl Trait haben.Von dynamischen Polymorphismus spricht man, falls Trait-Methoden aufgerufen werden und der Datentyp zur Kompilierzeit nicht bekannt ist: dyn Trait.
Da die Größe des Datentyps, der dyn Trait erfüllt, zur Kompilierzeit nicht bekannt ist (!Sized, siehe Abschnitt Marker Traits), kann dyn Trait alleine nicht genutzt werden. Vielmehr muss dyn Trait in einen Wide Pointer verpackt werden (z.B. &dyn Trait, Box<dyn Trait>, Arc<dyn Trait> etc.). Ein solcher Wide Pointer wird „Trait Object“ genannt und enthält neben der Adresse des Datentyps auch eine Virtual Table mit den Adressen der Methoden-Implementierungen.
Ein Trait Object ist möglich, falls der Trait „object safe“ ist:
SelfUpcasting von Traits eines Trait Objects ist seit Rust 1.86 möglich.
Einschränkungen:
Clone nicht object safe ist. Als Workaround kann das Crate dyn-clone genutzt werden.TODO
Traits können automatisch vereinigt werden:
trait C: A+B {}Datenstrukturen, sowie die für sie implementierten Methoden, können nicht automatisiert vereinigt werden.
TODO Bei generischen Typ-Parametern, Funktionsargumenten und -Return-Werten, sowie bei Trait-Definitionen.
Mehrere Traits können nur dann als Trait Bound einer Closure oder eines Trait Objects verwendet werden, wenn sie Auto Traits sind.
Marker Traits haben keine Methoden. Sie für einen Datentypen zu implementieren, bedeutet lediglich, den Compiler anzuweisen, diesen Datentyp besonders zu behandeln:
Copy: Implementiert ein Datentyp Copy, so kann er durch den Compiler (bei Zuweisung oder Übergabe an eine Funktion) implizit dupliziert werden.Send: Implementiert ein Datentyp Send, so kann höchstens 1 Thread exklusiven Zugang haben.Sized, falls er eine zur Kompilierzeit bekannte, endliche Größe hat. Nur solche Datentypen können auf dem Stack abgelegt werden. Für alle anderen Datentypen können nur Pointer auf sie auf dem Stack abgelegt werden.Sized, nicht jedoch bei Self in Traits, weil es Trait Objects verhindert.?Sized entfernt werden.Self: Sized sein. TODOSync: Implementiert ein Datentyp Sync, so können mehrere Threads gleichzeitig geteilten Zugang haben. Erfüllt ein Datentyp weder Send noch Sync, dann ist Zugang auf seine Instanz nur aus dem Thread möglich, aus dem die Instanz stammt.Unpin: TODODer Compiler validiert ggf. rekursiv, dass alle Bestandteile des Datentyps den Trait bzw. die Trait Bounds erfüllen.
Der kleinste Codebereich, in dem ein Datentyp instantiiert wird, ist der Besitzer (Owner) dieser Instanz. Nur der Besitzer hat auf sie Zugriff.
Der Besitz (Ownership) kann durch einen Move auf andere Codebereiche übergehen:
Zu jedem Zeitpunkt gibt es höchstens einen Besitzer. Der Compiler stellt sicher, dass Bindungen des vorherigen Besitzers nach einer Übertragung nicht mehr verwendet werden können.
Sobald Daten den besitzenden Codebereich verlassen, ohne dass sie von einem anderen Codebereich übernommen wurden, sind sie ungenutzt und ihr Speicherplatz kann und wird durch den Compiler freigegeben. Dies gilt auch für Freigabe anderer Ressourcen, wie geöffnete Dateien, Netzwerkverbindungen oder Locks.
Da immer höchstens ein Codebereich vollen Zugriff hat, sind Fehler aufgrund gleichzeitigen Zugriffs ausgeschlossen.
Die Besitzübertragung mittels Move ist außer bei primitiven Datentypen die Regel. Daten können vor der Übergabe dupliziert (geklont) werden, womit nur das Duplikat den Besitzer wechselt. Eine weitere Möglichkeit die Abgabe des Besitzes zu vermeiden ist das Borrowing (siehe nächster Abschnitt).
Die Duplizierung erfolgt über den Aufruf der Methode clone(), falls für den Datentyp der Trait Clone implementiert ist.
Ist für den Datentyp hingegen der Marker Trait Copy implementiert, erfolgt die Duplizierung an den entsprechenden Stellen durch den Compiler implizit. Ein expliziter Aufruf von clone() ist dann unnötig. rust-clippy weißt auf unnötigen Aufrufe von clone() hin.
Der Compiler implementiert Copy mittels bitweisem Kopieren. Bei Datentypen, wie String bzw. Vec, welche Pointer auf Bereiche im Heap halten, ist ein bitweises Kopieren unzulässig, da die duplizierten Pointer zu Speicherfehlern führen würden.
Mittels Borrowing kann anderen Codebereichen temporärer Zugriff auf Daten gewährt (ausgeliehen) werden, ohne dass der Besitz abgegeben werden muss.
Referenzen erlauben Zugriff auf zwei Arten:
&T: gewährt geteilten Zugriff&mut T: gewährt exklusiven ZugriffEine Lifetime gibt an, wie lange eine Referenz eines bestimmten Speicherbereichs gültig ist. Sie endet spätestens mit dem Drop der Daten, aber auch bereits mit einem Move.
Der Borrow Checker des Compilers stellt dies sicher. Kann er nicht eindeutig erkennen, dass die Lifetimes eingehalten werden, lehnt er den Code im Zweifel ab. Ein fälschliches Akzeptieren von Code würde dessen Speichersicherheit gefährden.
Die Entscheidung zwischen Copy und Borrowing ist nicht immer trivial.
TODO :
std::convert::AsRef: TODO z.B. AsRef<Path>.std::convert::From: TODOstd::convert::Into: TODOstd::ops::Deref: TODOstd::borrow::Borrow: TODOstd::borrow::Cow: TODO std::borrow::ToOwned ist die Umkehrfunktion.TODO Informed polling, cooperative scheduling.
async-Funktionen und -Blöcke werden in eine State Machine gewrappt. Der Returnwert-Typ ist nicht T, sondern impl Future<Output=T> + '_.Der Compiler kann nicht überprüfen:
.await nicht ausgeführt wirdWenn async Funktionen rekursiv aufgerufen werden sollen, muss der Result-Typ geboxed werden. Andernfalls hätte der Return-Wert-Typ — ähnlich wie bei rekursiven Enums oder Structs — unendliche Größe:
Send Bound: BoxFuture, zusammen mit FutureExt::boxed()Send Bound: LocalBoxFuture, zusammen mit FutureExt::boxed_local()Trait-Methoden können derzeit nicht async sein.
async Drop noch nicht möglich.Als Workaround kann das Crate async-trait verwendet werden.
Send Trait Bound kann man verhindern durch #[async_trait(?Send)] bei Deklaration und Implementation.Konstruktoren:
Destruktoren:
async:
async-Funktionen und -Blöcke werden in eine State Machine gewrappt. Der Returnwert-Typ ist nicht T, sondern impl Future<Output=T> + '_.deref() aus dem Trait Deref wird vom Compiler implizit aufgerufen.drop() wird implizit aufgerufen, sobald der Wert out-of-scope geht.for-Schleifen: rufen implizit die Methode into_iter() des Traits std::iter::IntoIterator auf.Mit bestimmtem Code kann der Compiler dazu genutzt werden, Eigenschaften von Datentypen entweder zu bestätigen oder mittels Fehlermeldungen zu erklären.
Erklären lassen:
let _: () = a;Bestätigen lassen:
die Object Safety des Traits T:
fn assert_object_safety(a: Box<dyn T>) {}die Thread-Sicherheit des Datentyps A:
fn assert_send<T: Send>() {}; assert_send::<A>();fn assert_sync<T: Sync>() {}; assert_sync::<A>();die Lifetime des Datentyps A:
fn assert_contravariance<'a>(a: A<'l>) -> A<'static> { a }fn assert_covariance<'a>(a: A<'static>) -> A<'l> { a }Smart Pointer erlauben, Einschränkungen des Borrow Checkers zu umgehen.
„Nicht thread-safe“ bedeutet für Rust, dass der Code nicht kompiliert, falls versucht würde, nicht thread-safe Datenstrukturen zwischen Threads auszutauschen.
im Modul std::cell:
für interior Mutability (Veränderung via Referenzen & anstatt unique Referenzen &mut):
falls etwa logisch immutable ist, technisch aber Mutation erfordert:
clone()Cell:
gibt keine Referenzen heraus, der Wert wird beim Schreiben und Lesen kopiert
UnsafeCellUnsafeCellim Modul std::rc:
Rc:
Weak:
im Modul std::sync:
Arc<Mutex<T>>: TODORc<RefCell<T>>: TODOBox, Vec nicht möglich. Für Box gibt es das experimentelle Attribut #![feature(box_syntax, box_patterns)].TODO
TODO
Code Coverage:
cargo tarpaulin --ignore-testsTODO
Abhängigkeiten:
nicht benötigte Abhängigkeiten komplett entfernen:
eigene Abhängigkeiten:
cargo tree: erleichtert zu erkennen, falls ein Crate in verschiedenen Versionen gleichzeitig verwendet wird.cargo-udeps: findet unbenutzte eigene AbhängigkeitenEs empfiehlt sich, die eigenen Abhängigkeiten zu aktualisieren, da diese wiederum ihre Abhängigkeiten optimiert haben könnten:
cargo updatecargo outdated: findet neuere Versionen der eigenen AbhängigkeitenAbhängigkeiten durch schlankere Altrnativen ersetzen:
cargo bloat --time --release -j 1cargo check verwenden statt cargo buildin Cargo.toml im Abschnitt [profile.dev] definieren:
split-debuginfo = "unpacked": TODOschnelleren Linker verwenden:
in Cargo.toml im Abschnitt [profile.release] definieren:
codegen-units = 1: TODOlto = "fat": TODO-C target-cpu=native: TODOpanic = "abort": TODOstrip="symbols": TODOBenchmarken:
die direkt und indirekt verwendeten Crates überprüfen:
nach Sicherheitslücken:
nach ungewünschten Lizenzen:
nach Benutzung von unsafe: