Rust ohne die Standardbibliothek: Eine eingehende Untersuchung der no_std-Entwicklung
Takashi Yamamoto
Infrastructure Engineer · Leapcell

Einführung
In der lebendigen Welt der Rust-Programmierung nehmen wir oft das reiche Ökosystem und die leistungsstarke std
-Bibliothek als selbstverständlich hin, die alles von Datenstrukturen bis hin zu Netzwerkfähigkeiten bereitstellt. Es ist eine komfortable, High-Level-Umgebung, die die Entwicklung für unzählige Anwendungen beschleunigt. Allerdings bieten nicht alle Computerumgebungen solche Luxusgüter. Stellen Sie sich Systeme mit extrem begrenztem Speicher, keinem Betriebssystem oder strengen Echtzeitbeschränkungen vor – denken Sie an eingebettete Geräte, Mikrocontroller oder sogar den Kern eines Betriebssystemkernels. In diesen Szenarien ist die std
-Bibliothek mit ihrer Abhängigkeit von OS-Diensten und dynamischer Speicherzuweisung eher ein Hindernis als eine Hilfe. Hier glänzt die no_std
-Programmierung in Rust. Sie ermöglicht es Entwicklern, hocheffizienten Bare-Metal-Code zu schreiben und Rusts Sicherheits- und Leistungsgarantien auf die wirklich eingeschränkten Umgebungen auszudehnen. Dieser Artikel befasst sich mit dem spannenden Bereich von no_std
, erklärt dessen Grundlagen, demonstriert seine Anwendung und zeigt, warum es ein unverzichtbares Werkzeug für eine wachsende Zahl von Entwicklern ist.
Die absoluten Grundlagen von no_std
Bevor wir uns auf unsere no_std
-Reise begeben, wollen wir die Schlüsselkonzepte, die diesem Programmierparadigma zugrunde liegen, klar verstehen.
Kernterminologie
no_std
: Dieses Attribut, das auf Crate-Ebene (#![no_std]
) angewendet wird, weist den Rust-Compiler an, nicht gegen die Standardbibliothek zu linken. Stattdessen linkt es gegen diecore
-Bibliothek, die grundlegende Sprachprimitive wieOption
,Result
, grundlegende Integer- und Fließkommatypen, Iteratoren und Slices bereitstellt, aber entscheidend keine OS-abhängigen Features oder dynamische Speicherzuweisung.std
library: Rusts Standardbibliothek, die einen reichhaltigen Satz von APIs für gängige Programmieraufgaben bereitstellt, einschließlich Datei-I/O, Netzwerk, Threading, Sammlungen (wieVec
undHashMap
) und dynamisches Speichermanagement.core
library: Die grundlegende Bibliothek für Rust, die von allen Rust-Programmen benötigt wird, auch vonno_std
-Programmen. Sie enthält das absolute Minimum, das Rust zum Funktionieren benötigt, einschließlich primitiver Typen, grundlegender Traits und grundlegender Fehlerbehandlung.alloc
crate: Ein optionales Crate, das gängige Sammlungstypen wieVec
undHashMap
bereitstellt, ohne von derstd
-Bibliothek abhängig zu sein, aber mit einer Abhängigkeit von einem globalen Allocator. Das bedeutet, Sie können diese dynamischen Datenstrukturen in einerno_std
-Umgebung verwenden, vorausgesetzt, Sie stellen einen Allocator bereit.- Allocator: Ein Mechanismus, der für die Verwaltung des dynamischen Speichers verantwortlich ist. In
std
-Umgebungen wird implizit ein Standard-System-Allocator verwendet. Inno_std
mitalloc
müssen Sie explizit einen globalen Allocator bereitstellen und registrieren. - Panic Handler: Wenn ein Rust-Programm auf einen nicht behebbaren Fehler stößt (z. B. ein Array-Zugriff außerhalb der Grenzen), "panict" es. In
std
-Umgebungen wird dies normalerweise als Backtrace ausgegeben und das Programm beendet. Inno_std
müssen Sie Ihren eigenen Panic-Handler definieren, da kein Betriebssystem den Panic abfangen oder darauf ausgeben kann. - Entry Point: Der Startpunkt Ihres Programms. In
std
-Programmen ist dies typischerweise die Funktionmain
. Inno_std
-Umgebungen, insbesondere Bare-Metal-Umgebungen, müssen Sie oft einen benutzerdefinierten Entry Point definieren, der normalerweise von einem Linkerskript verknüpft wird, um die anfängliche Einrichtung durchzuführen, bevor Ihremain
- oder äquivalente Funktion aufgerufen wird.
Prinzipien und Implementierung
Das Kernprinzip hinter no_std
ist die Eigenständigkeit. Ohne die Standardbibliothek sind Sie für die Verwaltung von Ressourcen, die Fehlerbehandlung und die direkte Interaktion mit Hardware oder über spezielle HALs (Hardware Abstraction Layers) verantwortlich.
Lassen Sie uns dies mit einem einfachen "Hallo, Welt!" für eine no_std
-Umgebung veranschaulichen, das zunächst auf die absoluten Grundlagen abzielt, ohne Druckfunktionen.
#![no_std] // WICHTIG: Aus der Standardbibliothek ausschließen #![no_main] // WICHTIG: Von der Standard-main-Funktion ausschließen use core::panic::PanicInfo; // Definieren Sie einen benutzerdefinierten Einstiegspunkt // Das `cortex-m-rt`-Crate stellt oft einen robusteren Einstiegspunkt für ARM-Mikrocontroller bereit. // Nur zu Illustrationszwecken tun wir dies hier manuell. #[no_mangle] // Stellen Sie sicher, dass der Linker diese Funktion anhand ihres Namens finden kann pub extern "C" fn _start() -> ! { // Ihr Initialisierungscode // Für ein echtes eingebettetes System könnte dies die Konfiguration von Takten, GPIO usw. umfassen. loop {} } // Definieren Sie unseren eigenen Panic-Handler #[panic_handler] fn panic(_info: &PanicInfo) -> ! { // In einer echten Anwendung könnte dies: // - Eine LED zum Anzeigen eines Fehlers einschalten // - Fehlerinformationen an einen seriellen Port protokollieren // - Einen System-Reset auslösen loop {} }
Dieses minimale Beispiel demonstriert die beiden kritischsten Aspekte von no_std
: #![no_std]
und einen benutzerdefinierten Panic-Handler. Die Funktion _start
dient als Einstiegspunkt unseres Programms, der normalerweise über ein Linkerskript für ein bestimmtes Ziel konfiguriert wird.
Verwendung des alloc
-Crates
Wenn Sie dynamische Sammlungen in einer no_std
-Umgebung benötigen, können Sie alloc
wieder einführen. Dies erfordert zwei Dinge: Aktivieren des alloc
-Features in Ihrer Cargo.toml
und Bereitstellen eines globalen Allocators.
Cargo.toml
:
[dependencies] # ... andere Abhängigkeiten ... alloc = { version = "0.0.0", package = "alloc" } # Verwenden Sie das `alloc`-Crate, es ist typischerweise "eingebaut", wird aber mit einem Feature aktiviert
Eigentlich ist alloc
nicht wie ein separates Crate, das Sie auf die gleiche Weise zu Cargo.toml
hinzufügen. Es ist ein bedingtes Kompilierungsziel des Rust-Compilers selbst. Um alloc
in Ihrem no_std
-Projekt zu aktivieren, verlassen Sie sich typischerweise auf Build-Tools oder Bibliotheken, die dies handhaben. Wenn Sie beispielsweise in einem Embedded-Projekt cortex-m-alloc
verwenden, würden Sie das alloc
-Feature für dieses spezifische Allocator-Crate aktivieren. Lassen Sie uns ein gängiges Muster für eingebettete Systeme verwenden:
Beispiel mit cortex-m-alloc
:
# Cargo.toml [dependencies] cortex-m = { version = "0.7.6", features = ["critical-section"] } cortex-m-rt = "0.7.0" cortex-m-alloc = "0.4.0" # Unser gewählter Allocator
src/main.rs
(oder src/lib.rs
für eine Bibliothek):
#![no_std] #![no_main] #![feature(alloc_error_handler)] // Benötigt für benutzerdefinierten Alloc-Fehlerhandler extern crate alloc; // Das `alloc`-Crate in den Gültigkeitsbereich holen use core::panic::PanicInfo; use alloc::vec::Vec; // Jetzt können wir Vec verwenden! // Einen globalen Allocator definieren #[global_allocator] static ALLOCATOR: cortex_m_alloc::CortexMHeap = cortex_m_alloc::CortexMHeap::empty(); // Allocator-Initialisierung // Dies würde typischerweise in Ihrer `_start`-Routine vor allen Zuweisungen erfolgen. // Zur Vereinfachung legen wir es hier in eine Setup-Funktion. fn init_allocator() { // Den Heap mit einem Speicherbereich initialisieren // In einem echten Programm würde dieser Speicherbereich in einem Linkerskript definiert // oder ein statisches Array sein. const HEAP_SIZE: usize = 1024; // 1KB Heap static mut HEAP_MEM: [u8; HEAP_SIZE] = [0; HEAP_SIZE]; // SICHERHEIT: Wir nehmen eine veränderliche Referenz auf ein statisches // und initialisieren den Allocator nur einmal. unsafe { ALLOCATOR.init(HEAP_MEM.as_ptr() as usize, HEAP_SIZE) } } // Unser benutzerdefinierter Einstiegspunkt #[cortex_m_rt::entry] // Bereitgestellt von cortex-m-rt für ARM-Mikrocontroller fn main() -> ! { init_allocator(); // Den Allocator initialisieren let mut my_vec: Vec<u32> = Vec::new(); my_vec.push(10); my_vec.push(20); // Wenn wir eine Möglichkeit zum Drucken hätten, würden wir hier my_vec drucken. // Zum Beispiel, indem wir es über eine serielle Schnittstelle senden. loop {} } #[panic_handler] fn panic(_info: &PanicInfo) -> ! { loop {} } // Einen benutzerdefinierten Fehlerhandler für Out-of-Memory-Fehler definieren #[alloc_error_handler] fn oom(_: core::alloc::Layout) -> ! { // Out-of-Memory-Fehler behandeln // z.B. eine LED blinken lassen, das System neu starten loop {} }
Dieses Beispiel zeigt, wie dynamische Zuweisungen gehandhabt werden. Der entscheidende Teil ist die Definition von #[global_allocator]
und die Bereitstellung der init
-Funktion, um ihm mitzuteilen, woher der Speicher verwaltet werden soll. Der tatsächliche Speicherbereich (HEAP_MEM
) würde typischerweise von Ihrem Embedded Build Environment Linker-Skript deklariert und verwaltet, um die richtige Platzierung zu gewährleisten.
Anwendungsbereiche
no_std
Rust ist keine reine akademische Übung; es ist ein leistungsstarker Ansatz für reale Anwendungen, bei denen Ressourcen von größter Bedeutung sind.
- Einbettete Systeme: Dies ist vielleicht der häufigste und überzeugendste Anwendungsfall. Mikrocontroller wie die ARM Cortex-M-Serie (z. B. in IoT-Geräten, Wearables, industriellen Steuerungen) verfügen über Kilobytes an RAM und Flash, weit zu wenig für ein vollständiges Betriebssystem und eine
std
-Bibliothek.no_std
Rust, kombiniert mit HALs, ermöglicht es Entwicklern, Low-Level-, Hochleistungs- und Typsicherheits-Firmware zu schreiben. - Betriebssystem-Kernel: Rust gewinnt in der Betriebssystementwicklung an Bedeutung. Das Schreiben eines Betriebssystem-Kernels erfordert direkte Hardware-Interaktion, sorgfältige Speicherverwaltung und keine Abhängigkeit von einem zugrunde liegenden Betriebssystem.
no_std
ist hier von grundlegender Bedeutung und ermöglicht es Entwicklern, Kernel von Grund auf neu zu erstellen und Rusts starkes Typsystem für Robustheit zu nutzen. - Bootloader: Der anfängliche Code, der ausgeführt wird, wenn ein System startet und für die Initialisierung der Hardware und das Laden des Hauptbetriebssystems oder der Anwendung verantwortlich ist. Bootloader arbeiten in einer stark eingeschränkten Umgebung und sind ein natürlicher Anwendungsfall für
no_std
. - Gerätetreiber: In einigen Bare-Metal- oder spezialisierten Betriebssystemumgebungen können Treiber in
no_std
Rust geschrieben werden, um direkt mit der Hardware zu interagieren, ohne eine vollständigestd
-Laufzeit einzubeziehen. - High-Performance Computing (HPC) / Wissenschaftliches Rechnen (Spezialisierte Fälle): Obwohl weniger verbreitet, könnten in Szenarien, die extreme Kontrolle über das Speicherlayout erfordern und jegliche OS-Level-Overheads für kritische Leistungspfade vermeiden,
no_std
-Bibliotheken oder Module in größerestd
-Anwendungen integriert werden, vorausgesetzt, sie verwalten ihren Speicher und ihre Interaktionen sorgfältig.
Fazit
Die no_std
-Programmierung in Rust eröffnet eine riesige neue Welt für Entwickler und erweitert Rusts gefeierte Sicherheits-, Leistungs- und Nebenläufigkeitsvorteile auf die ressourcenbeschränktesten und Bare-Metal-Umgebungen. Durch den Verzicht auf die Standardbibliothek und die Nutzung der core
-Bibliothek erhalten Entwickler eine feingranulare Kontrolle über den Footprint und das Verhalten ihres Codes, was Rust zu einer idealen Wahl für eingebettete Systeme, Betriebssystem-Kernel und andere Nischenanwendungen macht, bei denen jedes Byte und jede Zyklus zählt. Das Beherrschen von no_std
bedeutet nicht nur, Code ohne die std
-Bibliothek zu schreiben; es bedeutet, die grundlegenden Schichten des Computings zu verstehen und Rusts Leistung zu nutzen, um von Grund auf zuverlässige, effiziente Systeme aufzubauen.