Kapitel 19: Unsafe Rust und FFI – Systemnahe Integration und Speichersicherheit
Die Entwicklung von Betriebssystem-Kerneln, Treibern oder die Einbindung bestehender C-Bibliotheken erfordert fortgeschrittene Techniken von Unsafe Rust. In diesem Abschnitt betrachten wir die Details der fünf unsicheren Superkräfte sowie die Fremdsprachen-Schnittstelle (FFI).
1. Lernziele – Das wirst du heute lernen
- Die 5 Superkräfte beherrschen: Sie setzen alle Aspekte von
unsafesicher ein. - Globale Zustände verwalten: Sie verstehen die Risiken von
static mutund kennen sichere Alternativen. - Unions einsetzen: Sie verwenden überlappende Speicherbereiche für systemnahe Protokolle.
- C-Funktionen aufrufen (FFI): Sie binden externe Bibliotheken über
extern "C"ein. - Rust für C bereitstellen: Sie exportieren Funktionen mittels
#[no_mangle]. - Der Move-Fallstrick bei Zeigern: Sie verhindern hängende Zeiger bei Datenverschiebungen.
2. Die fünf Superkräfte im Detail
1. Rohe Zeiger dereferenzieren
Wie im Anfänger-Teil gezeigt, greifen Sie über *const T und *mut T direkt auf Speicheradressen zu.
2. Unsichere Funktionen aufrufen
Eine Funktion wird mit unsafe fn deklariert, wenn der Aufrufer bestimmte Vorbedingungen (Invarianten) einhalten muss, die der Compiler nicht prüfen kann:
#![allow(unused)]
fn main() {
/// # Sicherheit
/// Der Zeiger `ptr` darf nicht null sein und muss auf einen gültigen i32 zeigen.
pub unsafe fn absolut_unsicher(ptr: *const i32) -> i32 {
*ptr
}
}
3. Unsichere Traits implementieren
Ein Trait ist unsicher (unsafe trait), wenn die Implementierung Garantien geben muss, auf die sich sicherer Code blind verlässt. Beispiel:
#![allow(unused)]
fn main() {
unsafe trait Threadsicher {}
struct MeinTyp;
unsafe impl Threadsicher for MeinTyp {}
}
4. Globale veränderliche Variablen (static mut)
Globale Variablen sind in Rust standardmäßig unveränderlich. Möchten wir sie zur Laufzeit modifizieren, müssen wir sie als static mut deklarieren. Da dies in Multithreading-Umgebungen zu Datenrennen führen kann, ist jeder Lese- und Schreibzugriff darauf unsafe:
#![allow(unused)]
fn main() {
static mut ANZAHL: u32 = 0;
fn zaehlen() {
unsafe {
ANZAHL += 1;
}
}
}
5. Auf Felder einer union zugreifen
Eine union lässt alle Felder an derselben Speicheradresse beginnen. Da Rust beim Lesen nicht weiß, welcher Typ gerade aktiv ist, ist der Lesezugriff stets unsafe:
#![allow(unused)]
fn main() {
#[repr(C)]
union Daten {
zahl: u32,
byte: u8,
}
}
3. FFI: Fremdsprachen-Schnittstelle (Foreign Function Interface)
C-Bibliotheken aus Rust aufrufen
Wir deklarieren externe Signaturen in einem extern "C"-Block. Jeder Aufruf dieser Funktionen ist unsafe:
// Deklaration der C-Funktion
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
// Aufruf erfordert unsafe
let positiv = abs(-10);
println!("Absolut: {}", positiv);
}
}
Rust für C bereitstellen
Damit C-Programme eine Rust-Bibliothek aufrufen können, müssen wir Name Mangling (Namensverzerrung des Compilers) verhindern und das C-ABI deklarieren:
#![allow(unused)]
fn main() {
// #[no_mangle] zwingt den Compiler, den Namen exakt so in der Symboltabelle zu lassen.
#[no_mangle]
pub extern "C" fn addiere_in_rust(a: i32, b: i32) -> i32 {
a + b
}
}
4. Der Move-Fallstrick: Hängende Zeiger (Dangling Pointers)
Ein häufiger Fehler bei der Arbeit mit rohen Zeigern entsteht durch Rusts Verschiebe-Semantik. Wenn eine Variable im Speicher verschoben (moved) oder deallokiert wird, zeigt ein zuvor erstellter roher Zeiger auf eine ungültige Adresse:
fn main() {
let zeiger: *const String;
{
let text = String::from("Hallo");
zeiger = &text as *const String;
} // 'text' wird hier gelöscht!
// GEFAHR! 'zeiger' zeigt auf freigegebenen Speicher (Use After Free).
// Das Lesen führt zu undefiniertem Verhalten!
// unsafe { println!("{}", *zeiger); }
}