Rust als sichere Programmiersprache fĂĽr systemnahe und parallele Software
Softwareprobleme wie Race Conditions können parallele und nebenläufige Anwendungen unsicher machen. Rust kommt Entwicklern mit Überprüfungen entgegen.
- Dr. Stefan Lankes
- Dr. Jens Breitbart
Die Entwicklung von Software mit parallelen oder nebenläufigen Aspekten gilt häufig als eine der schwierigsten Aufgaben im Bereich der Softwareentwicklung, da Fehler nur schwer auffindbar sind. Zu den bekannteren Problemen, die im praktischen Einsatz zum Teil tragische Folgen hatten (s. Kasten "Therac-25"), zählen Softwarefehler durch Wettlaufsituation (Race Condition). Diese Fehler können in verschiedenen Programmiersprachen auftreten. Wie der folgende Artikel zeigt, bietet Rust eine Reihe von Überprüfungen zur Kompilierzeit, mit denen sich Wettlaufsituationen verhindern lassen.
"Therac-25"
Durch eine Wettlaufsituation (race condition) in der Software des Linearbeschleunigers Therac-25, der sowohl zum Röntgen als auch für therapeutische Bestrahlung in der Krebstherapie zum Einsatz kam, sind einige Patienten mit dem 100-fachen der geplanten Strahlendosis behandelt und dadurch schwer verletzt, einzelne sogar getötet worden. Ursächlich für den Fehler war jedoch nicht die verwendete Programmiersprache.
Vorteile von Rust
Rust ist eine junge, erstmals 2010 vorgestellte, Programmiersprache. Sie entstand aus einem persönlichen Projekt des Mozilla-Mitarbeiters Graydon Hoare. Im Jahr 2015 erschien eine erste stabile Version. Die Weiterentwickelung treibt hauptsächlich Mozilla Research voran. In der Zwischenzeit sind einige Komponenten des Firefox Browsers in Rust programmiert.
Rust verfolgt das Ziel, verschiedene Programmierparadigmen (funktionale, objektorientierte etc.) zu vereinen. Dabei soll sie so effizient wie C/C++ und so sicher wie eine Interpretersprache sein. Der sichere Umgang mit Speicher sowie die Vermeidung von Wettlaufsituation stellen ein Alleinstellungsmerkmal gegenüber anderen Programmiersprachen dar. So kann Rust eine sichere Speicherverwaltung ohne Garbage Collection garantieren – dies verspricht eine hohe Effizienz.
Wichtig für den Erfolg einer neuen Programmiersprache ist die Integration von existierendem Code. Mit Rust ist es möglich, C und alle kompatiblen Programmiersprachen einzubinden. Das folgende Programm verdeutlicht, wie sich aus Rust eine C-Funktion aufrufen lässt:
#[link(name = "hello")]
extern {
fn c_hello() -> c_int;
}
fn main() {
println!("Hello world from Rust!");
unsafe {
c_hello();
}
}
In diesem Beispiel ist unterstellt, dass eine Shared Library existiert, die libhello.so heißt und die C-Funktion c_hello enthält. Diese Funktion könnte zum Beispiel einen einfachen Text auf der Konsole ausgeben. Innerhalb des Kontrollblocks extern ist definiert, wie die Funktion für Rust aussieht. Sie heißt c_hello und gibt eine Ganzzahl zurück, c_int definiert explizit, dass der verwendete Datentyp in C ein int ist. Die Zeile vor dem Kontrollblock extern gibt an, in welcher Shared Library die Funktion zu suchen ist.
Ganzzahlen sind recht simple Datentypen und lassen sich einfach zwischen den Programmiersprachen austauschen. Aber auch komplexere Datenstrukturen, beispielsweise ein Struct, lassen sich zwischen Rust und C austauschen. Dabei ist lediglich zu gewährleisten, dass beide Programmiersprachen die gleiche Darstellungsform verwenden. Im folgenden Beispiel legt die Zeile oberhalb der Struct-Definition fest, dass der Struct das gleiche (Speicher-)Layout wie C verwendet:
#[repr(C)]
struct RustObject {
number: c_int
}
Da aus Sicht von Rust C eine unsichere Sprache darstellt, sind C-Funktionsaufrufe nur in unsafe-Blöcken erlaubt. Falls das Programm ein Speicherproblem (zum Beispiel durch Nutzung uninitialisierter Zeiger) besitzt, kann nur in solchen Blöcken das Problem auftreten. In allen anderen Bereichen garantiert der Rust-Compiler, dass dies nicht passiert. Dadurch reduziert sich die Fehlersuche auf wenige Zeilen Rust-Code.