So geht Unsafe Rust

24.07.2024 von Serdar Yegulalp
Was ist ‚unsafe‘ Code in Rust? Und wozu braucht man ihn? Dieser Artikel liefert Antworten.
Manchmal geht's nicht ohne Unsafe Rust.
Foto: piya saisawatdikul | shutterstock.com

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:

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 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:

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:

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!

Jetzt CW-Newsletter sichern

(fm)

Dieser Beitrag basiert auf einem Artikel unserer US-Schwesterpublikation Infoworld.