Es gibt diverse Gründe dafür, als Developer auf Rust zu setzen (aber auch einige, das nicht zu tun). Zum Beispiel ist die Programmiersprache:
schnell,
memory-safe (ohne Garbage Collection) und
mit qualitativ hochwertigen Tools ausgestattet.
Rust ermöglicht es erfahrenen Programmieren darüber hinaus auch, einige seiner Sicherheitsfunktionen selektiv zu umgehen - beispielsweise, um noch mehr Speed oder eine direkte Low-level Memory Manipulation zu realisieren. Diese "Spielart" wird gemeinhin als "Unsafe Rust" bezeichnet und beschreibt einen Codeblock, der durch das Keyword unsafe
abgegrenzt wird.
In diesem Artikel werfen wir einen Blick darauf, wozu Unsafe Rust eigentlich gut ist - und wie Sie es sinnvoll einsetzen.
Was mit Unsafe Rust möglich ist
Wie bereits beschrieben, kommt das Keyword unsafe
zum Einsatz, um einen Codeblock oder eine Funktion abzugrenzen und ein spezifisches Feature-Subset von Rust freizuschalten. Im Folgenden werfen wir einen Blick auf die Funktionen, die sich Ihnen mit unsafe
-Blöcken in Rust eröffnen.
Raw Pointer
Raw Pointer können sich in Rust auf veränderliche oder unveränderliche Werte beziehen und sind dabei deutlich näher an der Vision von C als an den Referenzen in Rust. Mit ihrer Hilfe können Sie einige Borrowing-Regeln über Bord werfen:
Raw Pointer können Nullwerte sein.
Mehrere, veränderbare Raw Pointer können auf denselben Speicherplatz verweisen.
Sowohl unveränderliche als auch veränderbare Pointer können auf denselben Speicherplatz verweisen.
Es ist nicht obligatorisch, dass der Raw Pointer auf einen gültigen Speicherbereich verweist.
Raw Pointer sind nützlich, um beispielsweise direkt auf Hardware zuzugreifen oder über einen "raw"-Speicherbereich mit einer Anwendung zu kommunizieren, die in einer anderen Sprache geschrieben wurde.
Externe Function Calls
Darüber hinaus wird unsafe
regelmäßig für Calls über ein Foreign Function Interface (FFI) verwendet. Dabei gibt es keine Garantie dafür, dass die Antwort auf einen solchen Call den Regeln von Rust folgt. Es besteht auch die Möglichkeit, dass Sie bestimmte Dinge zur Verfügung stellen müssen, die nicht diesen Regeln entsprechen (etwa Raw Pointer).
Werfen wir einen Blick auf folgendes Code-Beispiel (das der Rust-Dokumentation entstammt):
extern "C" {
fn abs(input: i32) -> i32;
}
fn main() {
unsafe {
println!("Absolute value of -3 according to C: {}", abs(-3));
}
}
Sämtliche Funktions-Calls, die über den extern "C"
-Block offengelegt werden, müssen mit unsafe
verpackt werden.
Veränderbare, statische Variablen
Globale oder statische Variablen können in Rust auf mutable
gesetzt werden, da sie eine festgelegte Speicheradresse belegen. Allerdings sind veränderbare, statische Variablen ausschließlich innerhalb eines unsafe
-Blocks anpassbar.
Der Hauptgrund, mit Hilfe von unsafe Mutable Static Variables (MSVs) anzupassen, sind Data Races. Würden Sie zulassen, dass dieselbe MSV über unterschiedliche Threads geändert wird, wären unvorhersehbare Ergebnisse die Folge - dessen sollten Sie sich bewusst sein, wenn Sie unsafe
zu diesem Zweck verwenden.
"Unsafe"-Methoden und -Traits
Methoden (Funktionen) können mit folgender Declaration "unsafe gemacht werden":
unsafe fn <function name>()
Damit stellen Sie sicher, dass jeder Call einer solchen Methode auch innerhalb eines unsafe-Blocks erfolgen muss. Wenn Sie beispielsweise eine Funktion haben, die einen Raw Pointer als Argument benötigt, sollten Sie sicherstellen dass der Caller seine Due-Diligence-Aufgaben erledigt hat. Darüber hinaus können Sie auch Traits zusammen mit ihren Implementierungen als unsafe
deklarieren. Dazu verwenden Sie folgende Syntax:
für den Trait:
unsafe trait <trait_name>
für die Implementierung:
unsafe impl <trait_name>
Im Gegensatz zu einer unsafe
-Methode muss eine solche Trait-Implementierung jedoch nicht innerhalb eines unsafe-Blocks aufgerufen werden. Die Security-Last liegt bei demjenigen, der die Implementierung schreibt, nicht bei dem, der sie aufruft.
Unions
In Rust sind Unions im Wesentlichen identisch zu denen in C: Es handelt sich um eine Struktur, die mehrere mögliche Typdefinitionen für ihren Content aufweist. Das ist für C akzeptabel - die strengen Regularien von Rust erlauben das jedoch standardmäßig nicht.
Allerdings sind Rust-Strukturen, die auf eine C Union mappen, manchmal unumgänglich. Beispielsweise, wenn es eine C-Bibliothek gecallt werden soll, um mit einer Union zu arbeiten. Um das umzusetzen, gilt es, mit unsafe
auf jeweils eine bestimmte Feld-Definition zuzugreifen. Im Folgenden ein weiteres Beispiel (diesmal aus dem Comprehensive Rust Guide):
#[repr(C)]
union MyUnion {
i: u8,
b: bool,
}
fn main() {
let u = MyUnion { i: 42 };
println!("int: {}", unsafe { u.i });
println!("bool: {}", unsafe { u.b }); // Undefined behavior!
}
Für jeden Zugriff auf die Union ist unsafe
zu verwenden. Der Borrow Checker erfordert außerdem, alle Felder einer Union zu "entleihen" - auch wenn Sie nur auf jeweils eines davon zugreifen wollen.
Dabei ist zu beachten, dass beim Schreiben einer Union nicht dieselben Restriktionen greifen: Die Vision von Rust ist, dass Sie nichts schreiben, das getrackt werden muss. Deshalb brauchen Sie auch unsafe
nicht, wenn Sie den Inhalt der Union mit dem let
-Statement definieren.
Was mit Unsafe Rust nicht geht
Das Keyword unsafe
zu verwenden, um den Borrow Checker zu umgehen, ist nicht möglich. Borrows werden immer noch auf Werte in unsafe
erzwungen, genauso wie überall sonst. Die Funktionsweise von Borrows und Referenzen sind eines der absolut unveränderlichen Rust-Prinzipien - daran ändert auch unsafe
nichts.
Idealerweise nehmen Sie unsafe als ein Sub- oder Superset von Rust wahr, das ein paar neue Funktionen hinzufügt, ohne dabei bestehende zu entfernen.
Best Practices für Unsafe Rust
Mit unsafe
verhält es sich wie mit den meisten anderen Sprach-Features: Es sollte mit Bedacht und Sorgfalt eingesetzt werden. Dabei helfen die folgenden Best Practices:
unsafe-Codeblöcke so klein wie möglich halten. Je überschaubarer ein unsafe-Block ist, desto besser. In vielen Fällen ist es nicht nötig, dass diese über einige wenige Codezeilen hinausgehen. Es lohnt sich deshalb, darüber nachzudenken, wie viel Codes tatsächlich 'unsafe' sein muss, und wie man Grenzen und Interfaces um diesen Code herum durchsetzt. Dabei empfiehlt es sich,
unsafe
-Code mit sicheren Interfaces zu kombinieren. Es kann allerdings manchmal auch vonnöten sein, dass auch die Interfaces selbstunsafe
sind. Wie bereits erwähnt, können ganze Funktionen alsunsafe
deklariert werden. Falls ein Argument einer Funktionunsafe
sein muss, gilt das auch für sämtliche Calls zu dieser Funktion. Ganz allgemein empfiehlt es sich jedoch, mit einzelnenunsafe
-Blöcken zu beginnen und diese nur bei Bedarf zu Funktionen zu erheben.Undefiniertes Verhalten beachten. Da Undefined Behavior in Rust existiert, gilt das auch für Unsafe Rust. Die grundlegenden Rust-Sicherheitsmaßnahmen schützen zum Beispiel nicht vor Data Races oder Lesevorgängen aus nicht initialisiertem Speicher. Besondere Vorsicht ist angebracht, wenn Sie undefiniertes Verhalten in
unsafe
-Blöcken einführen wollen. Weiterführende Infos zum Thema finden Sie hier.Gründe für unsafe dokumentieren. Bekanntermaßen sollten Code-Kommentare nicht beschreiben, was getan wird, sondern warum. Das trifft auch auf
unsafe
-Blöcke zu. Es ist zu empfehlen, wann immer möglich genau zu dokumentieren, warum einunsafe
-Block an einer bestimmten Stelle nötig ist. Das vermittelt anderen Devs eine Vorstellung davon, aus welchen Gründen unsafe verwendet wurde - und möglicherweise einen Anstoß dazu, den Code so zu umzuschreiben, dass das nicht mehr nötig ist.Rustonomicon lesen. Zur Dokumentation von Rust gehört auch das Rustonomicon, das sämtliche Details zu Unsafe Rust ausgiebig erörtert. Zuvor sollten Sie allerdings unbedingt die Grundlagen von Rust beherrschen. Falls Sie der "C-Fraktion" entstammen, ist "Learn Rust The Dangerous Way" eine weitere, empfehlenswerte Ressourcen-Sammlung.
Sie wollen weitere interessante Beiträge zu diversen Themen aus der IT-Welt lesen? Unsere kostenlosen Newsletter liefern Ihnen alles, was IT-Profis wissen sollten - direkt in Ihre Inbox!
(fm)
Dieser Beitrag basiert auf einem Artikel unserer US-Schwesterpublikation Infoworld.