Zum Inhalt springen

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.“

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:

  1. Einen Raw Pointer (Rohzeiger) dereferenzieren
  2. Eine unsafe-Funktion aufrufen
  3. Auf eine veränderbare static-Variable zugreifen oder sie ändern
  4. Einen unsafe-Trait implementieren
  5. Auf Felder einer union zugreifen

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.

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:

5
▶ Selbst ausprobieren

Einen 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.

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 hast
▶ Selbst ausprobieren

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:

7
0
▶ Selbst ausprobieren

Schau 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 jeden unsafe-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.

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.

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.

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 = 15
▶ Selbst ausprobieren

In 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.

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
-1
▶ Selbst ausprobieren

Index 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.

  • unsafe schaltet 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 du unsafe.
  • Verpack unsafe in eine kleine, sichere Funktion, die zuerst die Regeln prüft, und erklär in einem // SAFETY:-Kommentar, warum das korrekt ist.
  • unsafe gibt 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

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