Das Entdecken der Superkräfte von Unsafe Rust

RustRustBeginner
Jetzt üben

This tutorial is from open-source community. Access the source code

💡 Dieser Artikel wurde von AI-Assistenten übersetzt. Um die englische Version anzuzeigen, können Sie hier klicken

Einführung

Willkommen zu Unsafe Rust. Dieser Lab ist Teil des Rust Buchs. Du kannst deine Rust-Fähigkeiten in LabEx üben.

In diesem Lab werden wir uns mit unsafe Rust befassen, einem Feature, das uns ermöglicht, die bei der Kompilierung强制执行的内存安全garantien zu umgehen und uns zusätzliche Superkräfte verleiht, während wir gleichzeitig die mit seiner Verwendung verbundenen Risiken und Verantwortlichkeiten verstehen.

Unsafe Rust

Aller Code, den wir bisher besprochen haben, hat die von Rust gewährleisteten Speichersicherheiten bei der Kompilierung强制执行. Allerdings hat Rust eine zweite Sprache in sich versteckt, die diese Speichersicherheitsgarantien nicht durchsetzt: Sie heißt unsafe Rust und funktioniert wie regulärer Rust, gibt uns aber zusätzliche Superkräfte.

Unsafe Rust existiert, weil statische Analysen von Natur aus konservativ sind. Wenn der Compiler versucht, zu bestimmen, ob der Code die Garantien einhält, ist es besser, wenn er einige gültige Programme ablehnt, als wenn er einige ungültige Programme akzeptiert. Auch wenn der Code eventuell in Ordnung ist, wird der Rust-Compiler den Code ablehnen, wenn er nicht genug Informationen hat, um sich sicher zu sein. In diesen Fällen kannst du unsafe Code verwenden, um dem Compiler zu sagen: "Vertraue mir, ich weiß, was ich mache." Achte jedoch darauf, dass du unsafe Rust auf eigene Gefahr verwendest: Wenn du unsafe Code falsch verwendest, können Probleme aufgrund von Speichersicherheitsfehlern auftreten, wie z. B. das Dereferenzieren eines Nullzeigers.

Ein weiterer Grund, warum Rust eine unsichere Nebenperson hat, ist, dass die zugrunde liegende Computerhardware von Natur aus unsicher ist. Wenn Rust dir nicht erlaubte, unsichere Operationen durchzuführen, könntest du bestimmte Aufgaben nicht erledigen. Rust muss dir ermöglichen, low-level Systems-Programmierung durchzuführen, wie z. B. direkt mit dem Betriebssystem zu interagieren oder sogar dein eigenes Betriebssystem zu schreiben. Das Arbeiten mit low-level Systems-Programmierung ist eines der Ziele der Sprache. Lass uns untersuchen, was wir mit unsafe Rust tun können und wie wir es tun.

Unsafe Superkräfte

Um zu unsafe Rust zu wechseln, verwendest du das Schlüsselwort unsafe und startest dann einen neuen Block, in dem der unsafe Code steht. In unsafe Rust kannst du fünf Aktionen ausführen, die du in safe Rust nicht ausführen kannst, was wir als unsafe Superkräfte bezeichnen. Diese Superkräfte umfassen die Fähigkeit:

  1. Ein rohen Zeiger zu dereferenzieren
  2. Eine unsichere Funktion oder Methode aufzurufen
  3. Ein mutables statisches Variable zuzugreifen oder zu modifizieren
  4. Ein unsicheres Trait zu implementieren
  5. Felder von unions zuzugreifen

Es ist wichtig zu verstehen, dass unsafe den Borrow-Checker nicht deaktiviert oder keine anderen Sicherheitsüberprüfungen von Rust deaktiviert: Wenn du in unsafe Code eine Referenz verwendest, wird sie immer noch überprüft. Das Schlüsselwort unsafe gibt dir nur Zugang zu diesen fünf Funktionen, die dann von dem Compiler nicht auf Speichersicherheit überprüft werden. Du erhältst immer noch einen gewissen Grad an Sicherheit innerhalb eines unsafe Blocks.

Zusätzlich bedeutet unsafe nicht, dass der Code innerhalb des Blocks notwendigerweise gefährlich ist oder dass er definitiv Speichersicherheitsfehler haben wird: Die Absicht ist, dass als Programmierer du sicherstellst, dass der Code innerhalb eines unsafe Blocks den Speicher auf eine gültige Weise zugreift.

Menschen können fehlerhaft sein und Fehler werden passieren, aber indem diese fünf unsicheren Operationen in Blöcken mit unsafe annotiert sein müssen, weißt du, dass alle Fehler, die mit der Speichersicherheit zusammenhängen, innerhalb eines unsafe Blocks liegen müssen. Halte unsafe Blöcke klein; du wirst es später dankbar sein, wenn du Speicherfehler untersuchst.

Um unsafe Code so weit wie möglich zu isolieren, ist es am besten, diesen Code in eine sichere Abstraktion zu kapseln und eine sichere Schnittstelle bereitzustellen, über die wir später im Kapitel sprechen werden, wenn wir uns unsicheren Funktionen und Methoden widmen. Teile der Standardbibliothek werden als sichere Abstraktionen über unsafe Code implementiert, der überprüft wurde. Das Umhüllen von unsafe Code in eine sichere Abstraktion verhindert, dass die Verwendung von unsafe in alle Orte ausleckt, an denen du oder deine Benutzer die Funktionalität verwenden möchten, die mit unsafe Code implementiert wurde, denn das Verwenden einer sicheren Abstraktion ist sicher.

Lass uns nacheinander die fünf unsafe Superkräfte betrachten. Wir werden auch einige Abstraktionen betrachten, die eine sichere Schnittstelle zu unsafe Code bieten.

Ein rohen Zeiger dereferenzieren

Im Abschnitt "Schwebende Referenzen" haben wir erwähnt, dass der Compiler sicherstellt, dass Referenzen immer gültig sind. Unsafe Rust hat zwei neue Typen namens rohe Zeiger, die ähnlich zu Referenzen sind. Wie bei Referenzen können rohe Zeiger unveränderlich oder veränderlich sein und werden als *const T bzw. *mut T geschrieben. Das Sternchen ist kein Dereferenzierungsoperator; es ist Teil des Typnamens. Im Kontext von rohen Zeigern bedeutet unveränderlich, dass der Zeiger nach der Dereferenzierung nicht direkt zugewiesen werden kann.

Im Gegensatz zu Referenzen und Smart-Pointern erlauben rohe Zeiger:

  • Die Entlassungsregeln zu ignorieren, indem sowohl unveränderliche als auch veränderliche Zeiger oder mehrere veränderliche Zeiger auf die gleiche Adresse verweisen
  • Es ist nicht gewährleistet, dass sie auf gültigen Speicher verweisen
  • Es ist erlaubt, dass sie NULL sind
  • Es wird keine automatische Bereinigung implementiert

Indem du die Garantien von Rust nicht durchsetzen lässt, kannst du die garantierte Sicherheit aufgeben, um eine höhere Leistung oder die Möglichkeit zu erhalten, mit einer anderen Sprache oder Hardware zu interagieren, wo Rusts Garantien nicht gelten.

Listing 19-1 zeigt, wie man einen unveränderlichen und einen veränderlichen rohen Zeiger aus Referenzen erstellt.

let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

Listing 19-1: Erstellen von rohen Zeigern aus Referenzen

Bemerkenswert ist, dass wir in diesem Code nicht das Schlüsselwort unsafe verwenden. Wir können rohe Zeiger in safe Code erstellen; wir können sie nur außerhalb eines unsafe Blocks nicht dereferenzieren, wie du bald sehen wirst.

Wir haben rohe Zeiger erstellt, indem wir as verwenden, um eine unveränderliche und eine veränderliche Referenz in ihre entsprechenden rohen Zeigertypen umzuwandeln. Da wir sie direkt aus Referenzen erstellt haben, die als gültig gewährleistet sind, wissen wir, dass diese speziellen rohen Zeiger gültig sind, aber wir können dies für jeden beliebigen rohen Zeiger nicht annehmen.

Um dies zu demonstrieren, werden wir im nächsten Schritt einen rohen Zeiger erstellen, dessen Gültigkeit uns nicht so sicher ist. Listing 19-2 zeigt, wie man einen rohen Zeiger auf eine beliebige Adresse im Speicher erstellt. Versuchen, beliebigen Speicher zu verwenden, ist undefiniert: Es könnte Daten an dieser Adresse geben oder es könnte auch keine geben, der Compiler könnte den Code optimieren, so dass es keinen Speicherzugriff gibt, oder das Programm könnte mit einem Segmentation-Fehler beenden. Normalerweise gibt es keinen guten Grund, Code wie diesen zu schreiben, aber es ist möglich.

let address = 0x012345usize;
let r = address as *const i32;

Listing 19-2: Erstellen eines rohen Zeigers auf eine beliebige Speicheradresse

Denken wir daran, dass wir rohe Zeiger in safe Code erstellen können, aber wir können rohe Zeiger nicht dereferenzieren und die daraufzeigenden Daten lesen. In Listing 19-3 verwenden wir den Dereferenzierungsoperator * auf einen rohen Zeiger, der einen unsafe Block erfordert.

let mut num = 5;

let r1 = &num as *const i32;
let r2 = &mut num as *mut i32;

unsafe {
    println!("r1 is: {}", *r1);
    println!("r2 is: {}", *r2);
}

Listing 19-3: Dereferenzieren von rohen Zeigern innerhalb eines unsafe Blocks

Das Erstellen eines Zeigers schadet nicht; erst wenn wir versuchen, auf den Wert zuzugreifen, auf den er zeigt, können wir mit einem ungültigen Wert zu tun haben.

Beachte auch, dass in Listings 19-1 und 19-3 wir *const i32 und *mut i32 rohe Zeiger erstellt haben, die beide auf die gleiche Speicheradresse zeigten, an der num gespeichert ist. Wenn wir stattdessen versuchen würden, eine unveränderliche und eine veränderliche Referenz auf num zu erstellen, wäre der Code nicht kompiliert, weil Rusts Besitzregeln nicht zulassen, dass eine veränderliche Referenz gleichzeitig mit irgendeiner unveränderlichen Referenz existiert. Mit rohen Zeigern können wir einen veränderlichen Zeiger und einen unveränderlichen Zeiger auf die gleiche Adresse erstellen und durch den veränderlichen Zeiger Daten ändern, was möglicherweise einen Datenkonflikt erzeugt. Vorsicht!

Mit all diesen Gefahren, warum würdest du überhaupt rohe Zeiger verwenden? Ein wichtiger Anwendungsfall ist die Schnittstelle mit C-Code, wie du im Abschnitt "Aufrufen einer unsicheren Funktion oder Methode" sehen wirst. Ein weiterer Fall ist die Erstellung sicherer Abstraktionen, die der Borrow-Checker nicht versteht. Wir werden unsichere Funktionen einführen und dann ein Beispiel einer sicheren Abstraktion betrachten, die unsafe Code verwendet.

Aufrufen einer unsicheren Funktion oder Methode

Die zweite Art von Operation, die du in einem unsafe Block ausführen kannst, ist das Aufrufen von unsicheren Funktionen. Unsichere Funktionen und Methoden sehen genau wie reguläre Funktionen und Methoden aus, aber sie haben ein zusätzliches unsafe vor der restlichen Definition. Das Schlüsselwort unsafe in diesem Kontext zeigt an, dass die Funktion Anforderungen hat, die wir einhalten müssen, wenn wir diese Funktion aufrufen, weil Rust nicht gewährleisten kann, dass wir diese Anforderungen erfüllt haben. Indem wir eine unsichere Funktion innerhalb eines unsafe Blocks aufrufen, sagen wir Rust, dass wir die Dokumentation dieser Funktion gelesen haben und wir uns für die Einhaltung der Verträge der Funktion verantworten.

Hier ist eine unsichere Funktion namens dangerous, die im Wesentlichen nichts tut:

unsafe fn dangerous() {}

unsafe {
    dangerous();
}

Wir müssen die dangerous Funktion innerhalb eines separaten unsafe Blocks aufrufen. Wenn wir versuchen, dangerous ohne den unsafe Block aufzurufen, erhalten wir einen Fehler:

error[E0133]: call to unsafe function is unsafe and requires
unsafe function or block
 --> src/main.rs:4:5
  |
4 |     dangerous();
  |     ^^^^^^^^^^^ call to unsafe function
  |
  = note: consult the function's documentation for information on
how to avoid undefined behavior

Mit dem unsafe Block sagen wir Rust, dass wir die Dokumentation der Funktion gelesen haben, wir verstehen, wie wir sie richtig verwenden, und wir haben überprüft, dass wir die Verträge der Funktion erfüllen.

Die Körper von unsicheren Funktionen sind effektiv unsafe Blöcke, so dass wir keine zusätzlichen unsafe Blöcke hinzufügen müssen, um andere unsichere Operationen innerhalb einer unsicheren Funktion durchzuführen.

Erstellen einer sicheren Abstraktion über unsafe Code

Das bloße Vorhandensein von unsafe Code in einer Funktion bedeutet noch lange nicht, dass wir die gesamte Funktion als unsafe markieren müssen. Tatsächlich ist das Einhüllen von unsafe Code in eine sichere Funktion eine häufige Abstraktion. Als Beispiel betrachten wir die split_at_mut-Funktion aus der Standardbibliothek, die ein gewisses unsafe Code benötigt. Wir werden untersuchen, wie wir sie implementieren könnten. Diese sichere Methode wird für mutable Slices definiert: Sie nimmt einen Slice und teilt ihn an der als Argument angegebenen Indexposition in zwei Teile auf. Listing 19-4 zeigt, wie man split_at_mut verwendet.

let mut v = vec![1, 2, 3, 4, 5, 6];

let r = &mut v[..];

let (a, b) = r.split_at_mut(3);

assert_eq!(a, &mut [1, 2, 3]);
assert_eq!(b, &mut [4, 5, 6]);

Listing 19-4: Verwenden der sicheren split_at_mut-Funktion

Wir können diese Funktion nicht nur mit safe Rust implementieren. Ein Versuch könnte wie in Listing 19-5 aussehen, was jedoch nicht kompilieren wird. Um die Darstellung einfacher zu halten, implementieren wir split_at_mut als Funktion statt als Methode und nur für Slices von i32-Werten statt für einen generischen Typ T.

fn split_at_mut(
    values: &mut [i32],
    mid: usize,
) -> (&mut [i32], &mut [i32]) {
    let len = values.len();

    assert!(mid <= len);

    (&mut values[..mid], &mut values[mid..])
}

Listing 19-5: Ein versuchter Versuch zur Implementierung von split_at_mut mit nur safe Rust

Diese Funktion bestimmt zunächst die Gesamtlänge des Slices. Anschließend wird überprüft, ob der als Parameter angegebene Index innerhalb des Slices liegt, indem überprüft wird, ob er kleiner oder gleich der Länge ist. Die Prüfung bedeutet, dass die Funktion im Falle eines Indexes, der größer als die Länge ist, um das Teilen des Slices, vor dem Versuch, diesen Index zu verwenden, abstürzt.

Anschließend geben wir zwei mutable Slices in einem Tuple zurück: Einer vom Anfang des ursprünglichen Slices bis zum mid-Index und ein anderer von mid bis zum Ende des Slices.

Wenn wir den Code in Listing 19-5 versuchen, zu kompilieren, erhalten wir einen Fehler:

error[E0499]: cannot borrow `*values` as mutable more than once at a time
 --> src/main.rs:9:31
  |
2 |     values: &mut [i32],
  |             - let's call the lifetime of this reference `'1`
...
9 |     (&mut values[..mid], &mut values[mid..])
  |     --------------------------^^^^^^--------
  |     |     |                   |
  |     |     |                   second mutable borrow occurs here
  |     |     first mutable borrow occurs here
  |     returning this value requires that `*values` is borrowed for `'1`

Der Borrow-Checker von Rust kann nicht verstehen, dass wir verschiedene Teile des Slices borrowen; er weiß nur, dass wir von demselben Slice zweimal borrowen. Das Borrowen von verschiedenen Teilen eines Slices ist grundsätzlich in Ordnung, da die beiden Slices nicht überlappen, aber Rust ist nicht intelligent genug, um das zu wissen. Wenn wir wissen, dass der Code in Ordnung ist, aber Rust es nicht, ist es an der Zeit, unsafe Code zu verwenden.

Listing 19-6 zeigt, wie man einen unsafe-Block, einen rohen Zeiger und einige Aufrufe von unsicheren Funktionen verwendet, um die Implementierung von split_at_mut zu machen.

use std::slice;

fn split_at_mut(
    values: &mut [i32],
    mid: usize,
) -> (&mut [i32], &mut [i32]) {
  1 let len = values.len();
  2 let ptr = values.as_mut_ptr();

  3 assert!(mid <= len);

  4 unsafe {
        (
          5 slice::from_raw_parts_mut(ptr, mid),
          6 slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

Listing 19-6: Verwenden von unsafe Code in der Implementierung der split_at_mut-Funktion

Denken wir uns aus "Der Slice-Typ" zurück, dass ein Slice ein Zeiger auf einige Daten und die Länge des Slices ist. Wir verwenden die len-Methode, um die Länge eines Slices zu erhalten [1] und die as_mut_ptr-Methode, um auf den rohen Zeiger eines Slices zuzugreifen [2]. Im Falle eines mutable Slices von i32-Werten gibt as_mut_ptr einen rohen Zeiger vom Typ *mut i32 zurück, den wir in der Variable ptr gespeichert haben.

Wir behalten die Prüfung bei, dass der mid-Index innerhalb des Slices liegt [3]. Dann kommen wir zu dem unsafe Code [4]: Die slice::from_raw_parts_mut-Funktion nimmt einen rohen Zeiger und eine Länge und erstellt daraus einen Slice. Wir verwenden sie, um einen Slice zu erstellen, der bei ptr beginnt und mid Elemente lang ist [5]. Anschließend rufen wir die add-Methode auf ptr mit mid als Argument auf, um einen rohen Zeiger zu erhalten, der bei mid beginnt, und erstellen einen Slice mit diesem Zeiger und der verbleibenden Anzahl von Elementen nach mid als Länge [6].

Die Funktion slice::from_raw_parts_mut ist unsafe, weil sie einen rohen Zeiger nimmt und davon ausgehen muss, dass dieser Zeiger gültig ist. Die add-Methode auf rohen Zeigern ist ebenfalls unsafe, weil sie davon ausgehen muss, dass die Offsetposition ebenfalls ein gültiger Zeiger ist. Daher mussten wir einen unsafe-Block um unsere Aufrufe von slice::from_raw_parts_mut und add setzen, um sie aufrufen zu können. Indem wir den Code betrachten und die Prüfung hinzufügen, dass mid kleiner oder gleich len sein muss, können wir feststellen, dass alle rohen Zeiger, die innerhalb des unsafe-Blocks verwendet werden, gültige Zeiger auf Daten innerhalb des Slices sein werden. Dies ist eine akzeptable und angemessene Verwendung von unsafe.

Beachte, dass wir die resultierende split_at_mut-Funktion nicht als unsafe markieren müssen und wir diese Funktion aus safe Rust aufrufen können. Wir haben eine sichere Abstraktion für den unsafe Code mit einer Implementierung der Funktion erstellt, die unsafe Code auf sichere Weise verwendet, weil sie nur gültige Zeiger aus den Daten erstellt, auf die diese Funktion zugreift.

Im Gegensatz dazu würde die Verwendung von slice::from_raw_parts_mut in Listing 19-7 wahrscheinlich abstürzen, wenn der Slice verwendet wird. Dieser Code nimmt eine beliebige Speicheradresse und erstellt einen Slice, der 10.000 Elemente lang ist.

use std::slice;

let address = 0x01234usize;
let r = address as *mut i32;

let values: &[i32] = unsafe {
    slice::from_raw_parts_mut(r, 10000)
};

Listing 19-7: Erstellen eines Slices aus einer beliebigen Speicheradresse

Wir besitzen nicht den Speicher an dieser beliebigen Adresse, und es ist keine Garantie, dass der Slice, den dieser Code erstellt, gültige i32-Werte enthält. Das Versuchen, values als einen gültigen Slice zu verwenden, führt zu undefiniertem Verhalten.

Verwenden von externen Funktionen, um externen Code aufzurufen

Manchmal muss dein Rust-Code mit Code interagieren, der in einer anderen Sprache geschrieben wurde. Dazu hat Rust das Schlüsselwort extern, das die Erstellung und Verwendung einer Foreign Function Interface (FFI) erleichtert, was eine Möglichkeit ist, für eine Programmiersprache Funktionen zu definieren und es einer anderen (fremden) Programmiersprache zu ermöglichen, diese Funktionen aufzurufen.

Listing 19-8 zeigt, wie man eine Integration mit der abs-Funktion aus der C-Standardbibliothek einrichtet. Funktionen, die innerhalb von extern-Blöcken deklariert werden, sind immer unsicher, um von Rust-Code aus aufgerufen zu werden. Der Grund dafür ist, dass andere Sprachen die Regeln und Garantien von Rust nicht durchsetzen und Rust diese nicht überprüfen kann, sodass die Verantwortung bei dem Programmierer liegt, um die Sicherheit zu gewährleisten.

Dateiname: src/main.rs

extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!(
            "Absolute value of -3 according to C: {}",
            abs(-3)
        );
    }
}

Listing 19-8: Deklarieren und Aufrufen einer extern-Funktion, die in einer anderen Sprache definiert ist

Innerhalb des extern "C"-Blocks listieren wir die Namen und Signaturen von externen Funktionen aus einer anderen Sprache, die wir aufrufen möchten. Der "C"-Teil definiert, welche application binary interface (ABI) die externe Funktion verwendet: Das ABI definiert, wie die Funktion auf Assembly-Ebene aufgerufen wird. Das "C"-ABI ist der am häufigsten verwendete und folgt dem ABI der C-Programmiersprache.

Aufrufen von Rust-Funktionen aus anderen Sprachen

Wir können auch extern verwenden, um eine Schnittstelle zu erstellen, die anderen Sprachen ermöglicht, Rust-Funktionen aufzurufen. Anstatt einen ganzen extern-Block zu erstellen, fügen wir das extern-Schlüsselwort hinzu und geben das zu verwendende ABI vor dem fn-Schlüsselwort für die relevante Funktion an. Wir müssen auch eine #[no_mangle]-Annotation hinzufügen, um dem Rust-Compiler zu sagen, dass er den Namen dieser Funktion nicht verzerren soll. Verzerren ist, wenn ein Compiler den Namen, den wir einer Funktion gegeben haben, in einen anderen Namen umwandelt, der mehr Informationen für andere Teile des Kompilierungsprozesses enthält, aber weniger menschlich lesbar ist. Jeder Programmiersprachen-Compiler verzerren die Namen etwas unterschiedlich, sodass für eine Rust-Funktion, die von anderen Sprachen benannt werden soll, wir den Namenverzerrung des Rust-Compilers deaktivieren müssen.

Im folgenden Beispiel wird die call_from_c-Funktion für C-Code zugänglich gemacht, nachdem sie in eine Shared Library kompiliert und von C aus verknüpft wurde:

#[no_mangle]
pub extern "C" fn call_from_c() {
    println!("Just called a Rust function from C!");
}

Diese Verwendung von extern erfordert keine unsafe.

Zugreifen auf oder Ändern einer veränderlichen statischen Variable

In diesem Buch haben wir bisher noch nicht über globale Variablen gesprochen, die Rust zwar unterstützt, aber mit den Besitzregeln von Rust problematisch sein können. Wenn zwei Threads auf die gleiche veränderliche globale Variable zugreifen, kann dies einen Datenkonflikt verursachen.

In Rust werden globale Variablen als statische Variablen bezeichnet. Listing 19-9 zeigt ein Beispiel für die Deklaration und Verwendung einer statischen Variable mit einem String-Slice als Wert.

Dateiname: src/main.rs

static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("value is: {HELLO_WORLD}");
}

Listing 19-9: Definieren und Verwenden einer unveränderlichen statischen Variable

Statische Variablen ähneln Konstanten, über die wir in "Konstanten" gesprochen haben. Die Namen von statischen Variablen folgen der Konvention SCREAMING_SNAKE_CASE. Statische Variablen können nur Referenzen mit der Lebensdauer 'static speichern, was bedeutet, dass der Rust-Compiler die Lebensdauer ermitteln kann und wir nicht explizit annotieren müssen. Das Zugreifen auf eine unveränderliche statische Variable ist sicher.

Ein subtiler Unterschied zwischen Konstanten und unveränderlichen statischen Variablen ist, dass die Werte in einer statischen Variable eine feste Adresse im Speicher haben. Das Verwenden des Werts wird immer auf die gleichen Daten zugreifen. Konstanten hingegen sind erlaubt, ihre Daten jedes Mal zu duplizieren, wenn sie verwendet werden. Ein weiterer Unterschied ist, dass statische Variablen veränderlich sein können. Das Zugreifen auf und das Ändern von veränderlichen statischen Variablen ist unsicher. Listing 19-10 zeigt, wie man eine veränderliche statische Variable namens COUNTER deklariert, zugreift und modifiziert.

Dateiname: src/main.rs

static mut COUNTER: u32 = 0;

fn add_to_count(inc: u32) {
    unsafe {
        COUNTER += inc;
    }
}

fn main() {
    add_to_count(3);

    unsafe {
        println!("COUNTER: {COUNTER}");
    }
}

Listing 19-10: Das Lesen aus oder Schreiben in eine veränderliche statische Variable ist unsicher.

Wie bei regulären Variablen verwenden wir das mut-Schlüsselwort, um Veränderbarkeit anzugeben. Jeder Code, der von COUNTER liest oder schreibt, muss innerhalb eines unsafe-Blocks sein. Dieser Code kompiliert und druckt wie erwartet COUNTER: 3, da es ein einzelner Thread ist. Wenn mehrere Threads auf COUNTER zugreifen, würde wahrscheinlich ein Datenkonflikt auftreten.

Bei veränderlichen Daten, die global zugänglich sind, ist es schwierig, sicherzustellen, dass es keine Datenkonflikte gibt, weshalb Rust veränderliche statische Variablen als unsicher betrachtet. Wo möglich, ist es besser, die in Kapitel 16 diskutierten Konkurrenztechniken und thread-sicheren Smart-Pointer zu verwenden, damit der Compiler überprüft, dass der Datenzugang von verschiedenen Threads sicher erfolgt.

Implementieren eines unsicheren Traits

Wir können unsafe verwenden, um ein unsicheres Trait zu implementieren. Ein Trait ist unsicher, wenn mindestens eine seiner Methoden eine Invariante hat, die der Compiler nicht verifizieren kann. Wir deklarieren, dass ein Trait unsafe ist, indem wir das unsafe-Schlüsselwort vor trait hinzufügen und die Implementierung des Traits ebenfalls als unsafe markieren, wie in Listing 19-11 gezeigt.

unsafe trait Foo {
    // Methoden gehen hier
}

unsafe impl Foo for i32 {
    // Methodenimplementierungen gehen hier
}

Listing 19-11: Definieren und Implementieren eines unsicheren Traits

Durch die Verwendung von unsafe impl versprechen wir, dass wir die Invarianten einhalten werden, die der Compiler nicht verifizieren kann.

Als Beispiel erinnern wir uns an die Send- und Sync-Marker-Traits, über die wir in "Erweiterbare Konkurrenz mit den Send- und Sync-Traits" diskutiert haben: Der Compiler implementiert diese Traits automatisch, wenn unsere Typen ausschließlich aus Send- und Sync-Typen bestehen. Wenn wir einen Typ implementieren, der einen Typ enthält, der nicht Send oder Sync ist, wie z. B. rohe Zeiger, und wir diesen Typ als Send oder Sync markieren möchten, müssen wir unsafe verwenden. Rust kann nicht verifizieren, dass unser Typ die Garantien einhält, dass er sicher über Threads verschickt werden kann oder von mehreren Threads aus zugegriffen werden kann; daher müssen wir diese Prüfungen manuell vornehmen und dies mit unsafe anzeigen.

Zugreifen auf Felder einer Union

Die letzte Aktion, die nur mit unsafe funktioniert, ist das Zugreifen auf Felder einer Union. Eine Union ähnelt einer Struct, aber nur ein deklariertes Feld wird in einem bestimmten Instanz einmal verwendet. Unions werden hauptsächlich verwendet, um mit Unions in C-Code zu interagieren. Das Zugreifen auf Union-Felder ist unsicher, weil Rust nicht gewährleisten kann, welchen Typ die Daten haben, die derzeit in der Union-Instanz gespeichert sind. Sie können mehr über Unions in der Rust-Referenz unter https://doc.rust-lang.org/reference/items/unions.html lernen.

Wann man unsicheren Code einsetzen soll

Das Verwenden von unsafe, um eine der fünf zuvor besprochenen Superkräfte zu nutzen, ist nicht falsch und wird auch nicht missbilligt, aber es ist schwieriger, unsicheren Code richtig zu schreiben, da der Compiler nicht helfen kann, die Arbeitsspeichersicherheit zu gewährleisten. Wenn du einen Grund hast, unsicheren Code zu verwenden, kannst du es tun, und die explizite unsafe-Annotation macht es einfacher, die Problemquelle zu finden, wenn Probleme auftreten.

Zusammenfassung

Herzlichen Glückwunsch! Du hast das Unsafe Rust-Labor abgeschlossen. Du kannst in LabEx weitere Labore absolvieren, um deine Fähigkeiten zu verbessern.