Unsafe Rust
Jede Garantie, über die wir in diesem Kurs gesprochen haben – keine hängenden Referenzen, keine Data Races, kein Zugriff außerhalb der Grenzen –, setzt der Compiler durch. Mit unsafe sagst du dem Compiler: „Lass mich etwas tun, bei dem du nicht prüfen kannst, ob es sicher ist. Ich übernehme stattdessen selbst die Verantwortung.“
Was unsafe tut – und was nicht
Abschnitt betitelt „Was unsafe tut – und was nicht“Ein weit verbreiteter Irrtum ist, dass unsafe die Sicherheitsprüfungen von Rust komplett abschaltet. Das stimmt nicht. In einem unsafe-Block darfst du genau fünf zusätzliche Dinge tun, die sonst verboten sind:
- Einen Raw Pointer (Rohzeiger) dereferenzieren
- Eine
unsafe-Funktion aufrufen - Auf eine veränderbare
static-Variable zugreifen oder sie ändern - Einen
unsafe-Trait implementieren - Auf Felder einer
unionzugreifen
Alles andere – die Borrowing-Regeln, die Move-Semantik, die Typprüfung – gilt auch in unsafe-Code weiterhin vollständig. unsafe ist ein Skalpell für fünf bestimmte Operationen, kein Ausschalter.
Raw Pointer
Abschnitt betitelt „Raw Pointer“Die normalen Referenzen in Rust (& und &mut) zeigen immer auf gültige, korrekt ausgerichtete Daten. Das gehört zu dem, was der Compiler garantiert. Ein Raw Pointer (*const T oder *mut T) verzichtet auf diese Garantie. Deshalb brauchst du unsafe, um ihn zu lesen:
fn main() { let value = 5; let pointer = &value as *const i32;
unsafe { println!("{}", *pointer); }}Ausgabe:
5Einen Raw Pointer erzeugen darfst du auch in sicherem Code. Nur das Dereferenzieren (also den Wert lesen, auf den er zeigt) braucht unsafe, denn der Compiler kann nicht mehr prüfen, ob der Zeiger gültig ist.
Eine unsafe-Funktion aufrufen
Abschnitt betitelt „Eine unsafe-Funktion aufrufen“Manche Funktionen sind selbst als unsafe markiert, weil sie Voraussetzungen haben, die der Compiler nicht für dich prüfen kann. Wenn du sie aufrufst, versprichst du damit, dass du diese Voraussetzungen selbst geprüft hast:
unsafe fn dangerous() { println!("diese Funktion vertraut darauf, dass du ihre Voraussetzungen geprüft hast");}
fn main() { unsafe { dangerous(); }}Ausgabe:
diese Funktion vertraut darauf, dass du ihre Voraussetzungen geprüft hastUnsafe in eine sichere Funktion verpacken
Abschnitt betitelt „Unsafe in eine sichere Funktion verpacken“Die Standardbibliothek hat echte unsafe-Funktionen, die du aufrufen kannst. Eine davon ist get_unchecked auf Slices. Sie funktioniert wie numbers[i], lässt aber die Prüfung der Grenzen weg. Liegt i außerhalb des gültigen Bereichs, gibt es keine Panic. Die Funktion liest einfach den Speicher, der gerade an dieser Stelle liegt. Das nennt man undefiniertes Verhalten (Undefined Behavior): Dein Programm kann abstürzen, Unsinn ausgeben oder scheinbar funktionieren – bis es das eines Tages nicht mehr tut.
Die Regel für den Aufruf lautet also: „nur mit einem Index, von dem du weißt, dass er gültig ist“. Das übliche Muster: Du prüfst diese Regel selbst, und zwar in einer normalen, sicheren Funktion:
fn first_or_zero(numbers: &[i32]) -> i32 { if numbers.is_empty() { return 0; } // SAFETY: Der Slice ist nicht leer, also liegt Index 0 innerhalb der Grenzen. unsafe { *numbers.get_unchecked(0) }}
fn main() { println!("{}", first_or_zero(&[7, 8, 9])); println!("{}", first_or_zero(&[]));}Ausgabe:
70Schau dir an, was der Aufrufer sieht: fn first_or_zero(numbers: &[i32]) -> i32. In der Signatur steht kein unsafe, und man kann die Funktion nicht falsch aufrufen. Sie prüft die Regel nämlich, bevor sie überhaupt beim unsafe-Block ankommt. Das unsafe bleibt klein und im Inneren versteckt.
Zwei Gewohnheiten, an die sich erfahrene Rust-Entwickler halten:
- Schreib einen
// SAFETY:-Kommentar über jedenunsafe-Block. Darin steht, warum die Regeln eingehalten werden. Das hilft der nächsten Person, die den Code liest – und deinem zukünftigen Ich. - Halte den
unsafe-Block so klein wie möglich. Dann musst du weniger Code doppelt prüfen.
Ganz ehrlich: Genau diese Funktion würdest du in echtem Code nicht schreiben. numbers.first() erledigt dasselbe sicher, und der Compiler ist sehr gut darin, Grenzprüfungen wegzulassen, von denen er beweisen kann, dass sie unnötig sind. Aber das Muster (erst die Regeln prüfen, dann unsafe innerhalb einer sicheren Funktion aufrufen) ist genau so, wie Vec, String und Co. gebaut sind. Die Seite FFI nutzt denselben Trick, um C-Funktionen zu verpacken.
Warum es unsafe überhaupt gibt
Abschnitt betitelt „Warum es unsafe überhaupt gibt“Wenn sich bei Rust alles um Sicherheit dreht, warum erlaubt die Sprache so etwas dann überhaupt? Weil sich manche Dinge beim Kompilieren einfach nicht als sicher beweisen lassen, obwohl sie in der Praxis völlig sicher sind: direkt mit Hardware sprechen, eine C-Bibliothek aufrufen (siehe FFI) oder bestimmte hochperformante Datenstrukturen bauen. Mit unsafe kann die Standardbibliothek (und, selten, dein eigener Code) diese Low-Level-Bausteine umsetzen. Alles, was darauf aufbaut, bleibt ganz normales, vollständig geprüftes, sicheres Rust.
Der ehrliche Kompromiss
Abschnitt betitelt „Der ehrliche Kompromiss“Wenn du unsafe schreibst, ist dein Code dadurch nicht automatisch fehlerhaft. Es bedeutet nur, dass der Compiler in genau diesem Block bestimmte Arten von Fehlern nicht mehr für dich finden kann. Du musst dieselben Garantien dann selbst von Hand einhalten. Die meisten erfahrenen Rust-Entwickler schreiben nur sehr wenig unsafe-Code direkt. Er steckt meistens in kleinen, sorgfältig geprüften Ecken ganz unten in einer Bibliothek, verpackt in eine sichere API, die alle anderen benutzen.
Übungen
Abschnitt betitelt „Übungen“1. Einen Wert über einen Raw Pointer ändern
Abschnitt betitelt „1. Einen Wert über einen Raw Pointer ändern“Leg let mut score = 10; an, erzeuge einen Raw Pointer *mut i32 darauf und addiere über den Zeiger 5. Gib danach score aus. Tipp: Den Zeiger erzeugst du mit &mut score as *mut i32, und *pointer += 5 schreibst du in einen unsafe-Block.
Lösung anzeigen
fn main() { let mut score = 10; let pointer = &mut score as *mut i32; // ihn zu erzeugen ist sicher
unsafe { *pointer += 5; // über ihn zu schreiben nicht }
println!("Punkte = {score}");}Ausgabe:
Punkte = 15In der Praxis ist das sicher: score lebt noch, und während wir schreiben, benutzt nichts anderes die Variable. Sobald du unsafe verwendest, ist es deine Aufgabe, solche Versprechen einzuhalten.
2. Ein sicheres get_or
Abschnitt betitelt „2. Ein sicheres get_or“Schreib fn get_or(numbers: &[i32], index: usize, default: i32) -> i32. Die Funktion gibt das Element an der Stelle index zurück oder default, wenn der Index außerhalb des gültigen Bereichs liegt. Lies den Wert mit get_unchecked und sorg dafür, dass niemand die Funktion so aufrufen kann, dass ihre Regel verletzt wird. Tipp: Prüf zuerst index < numbers.len() und ruf get_unchecked nur auf, wenn das stimmt.
Lösung anzeigen
fn get_or(numbers: &[i32], index: usize, default: i32) -> i32 { if index < numbers.len() { // SAFETY: Wir haben gerade geprüft, dass index innerhalb der Grenzen liegt. unsafe { *numbers.get_unchecked(index) } } else { default }}
fn main() { let numbers = [10, 20, 30]; println!("{}", get_or(&numbers, 1, -1)); println!("{}", get_or(&numbers, 5, -1));}Ausgabe:
20-1Index 5 kommt nie beim unsafe-Block an, er wird also sicher behandelt. Zum Vergleich: Die vollständig sichere Version ist numbers.get(index).copied().unwrap_or(default), und die würdest du normalerweise schreiben.
Zusammenfassung
Abschnitt betitelt „Zusammenfassung“unsafeschaltet genau fünf Operationen frei (vor allem Raw Pointer und das Aufrufen von unsafe-Funktionen) – alles andere wird weiterhin vollständig geprüft.- Einen Raw Pointer (
*const T/*mut T) kannst du in sicherem Code erzeugen. Zum Dereferenzieren brauchst duunsafe. - Verpack
unsafein eine kleine, sichere Funktion, die zuerst die Regeln prüft, und erklär in einem// SAFETY:-Kommentar, warum das korrekt ist. unsafegibt es, damit Low-Level-Code (Hardwarezugriff, Zusammenarbeit mit C) gebaut werden kann, während der Code, der ihn nutzt, sicher bleibt.- Als Anfänger musst du es selten selbst schreiben – greif zuerst zu einer sicheren Abstraktion.
Kurzer Check
Abschnitt betitelt „Kurzer Check“Kurzer Check
1. Was schaltet ein unsafe-Block wirklich frei?
2. Kannst du in sicherem Code einen Raw Pointer erzeugen?
3. Wie oft wirst du als Anfänger selbst unsafe schreiben müssen?
4. Bedeutet unsafe-Code, dass dein Programm jetzt Fehler hat?
Punkte: 0 / 4
Weiterlesen
Abschnitt betitelt „Weiterlesen“- Unsafe Rust – Das Rust-Buch: die fünf „unsicheren Superkräfte“ im Detail, auch wie du ein sicheres
split_at_mutauf Basis von Raw Pointern baust. - The Rustonomicon (auf Englisch): die offizielle Anleitung, wie du unsafe-Code richtig schreibst. Fortgeschritten, aber das erste Kapitel lohnt sich sehr.
- Das Schlüsselwort
unsafe– Doku der Standardbibliothek (auf Englisch): unsafe-Blöcke, -Funktionen und -Traits, dazu die Konvention mit dem// SAFETY:-Kommentar. - Unsafety – The Rust Reference (auf Englisch): die genaue Liste der Operationen, die
unsafebrauchen. slice::get_unchecked– Doku der Standardbibliothek (auf Englisch): eine echte unsafe-Funktion. Ihr Abschnitt „Safety“ beschreibt genau die Regel, die du einhalten musst.