Von
/19.02.25

Bild von Enry B King auf Unsplash

Bei Renuo lieben wir Ruby. Es ist einfach, elegant und leistungsstark. Aber seien wir ehrlich, Ruby ist nicht die schnellste Sprache da draussen.

In den letzten Monaten habe ich mich mit der Low-Level-Programmierung beschäftigt, in der Hoffnung, die Lücke zwischen der High-Level-Welt von Ruby und der Low-Level-Welt der Systemprogrammierung zu überbrücken. Dazu habe ich mit meinem ersten Rust-Projekt begonnen: einem unglaublich schnellen Voxel-„Spiel“ namens rsmc. Es verfügt über einen Terrain-Generator, Meshing, eine skalierbare Client-Server-Architektur und benutzerdefinierte serialisierte Nachrichten für Hochgeschwindigkeitskommunikation. Dieses Projekt war mein Spielplatz, um Rust zu lernen, und in diesem Beitrag werde ich einige der Lektionen teilen, die ich auf dem Weg gelernt habe.

Frühes Entwicklungsstadium in RSMC. Renet-Visualisierer für gleichzeitige Client/Server-Verbindungen.

Warum Rust?

Rust ist eine der am meisten geschätzten Programmiersprachen, wie die GitHub Octoverse-Umfrage zeigt. Es bietet Speichersicherheit, hohe Leistung und starke Werkzeuge, was es zu einer soliden Wahl für sowohl kleine Hilfsprogramme als auch gross angelegte Anwendungen macht. Viele der Tools, die ich täglich verwende, wie Alacritty und 1Password, profitieren von Rusts Geschwindigkeit und Zuverlässigkeit.

Hauptvorteile

  • Leistung: Vergleichbar mit C und C++, aber mit Sicherheitsmechanismen, die häufige Fehler verhindern.

  • Speichersicherheit: Beseitigt Nullzeiger-Dereferenzierungen, Segmentierungsfehler und Datenrennen.

  • Moderne Syntax: Lesbar und ausdrucksstark, was es trotz seiner Low-Level-Fähigkeiten zugänglich macht.

  • Leistungsstarke Werkzeuge: Cargo vereinfacht die Abhängigkeitsverwaltung, das Bauen und das Testen.

Für mein Voxel-Spiel machen Rusts Geschwindigkeit und Sicherheit es zu einer ausgezeichneten Wahl für Terrain-Generierung, Netzwerke und Echtzeit-Interaktionen. Im Gegensatz zu dynamisch typisierten Sprachen wie Ruby fängt Rust ganze Kategorien von Fehlern zur Kompilierzeit ab, was die Wartbarkeit verbessert.

Neben CLI-Tools treibt Rust Spiel-Engines, Betriebssysteme, Simulationen und sogar Webbrowser an, was seine Anpassungsfähigkeit in verschiedenen Domänen beweist.

Bevy: Spielentwicklung mit Spass

Da mein Projekt mit Bevy gebaut wurde, war das Verständnis seiner Kernkonzepte sehr wichtig. Bevys Entity-Component-System (ECS)-Architektur macht die Spielentwicklung modular und effizient, indem sie hochgradig entkoppelte Systeme ermöglicht.

Einige meiner Lieblingserkenntnisse:

  • Systeme: Halte sie klein und auf eine Aufgabe konzentriert. So sind sie leichter zu testen und zu erweitern.

  • Plugins: Kapsle Ressourcen, Systeme und Komponenten in unterscheidbare Module.

  • Ereignisse: Nutze Ereignisse in vollem Umfang, um Systeme zu entkoppeln und den Code modular zu halten.

  • Zustände: Führe Systeme nur aus, wenn sie relevant sind (z.B. Menü, Spielen). Dies hilft bei der Trennung von UI und Logik. Insbesondere dieser PR: #32

Bevy macht die Strukturierung einer Spiel-Engine intuitiv, und sein Rust-first-Ansatz gewährleistet Sicherheit und Leistung, während es flexibel bleibt. Wenn du mehr über den ECS-Ansatz zur Spielentwicklung lernen möchtest, habe ich einen Blogartikel über die Planung eines ECS geschrieben: Multiplayer in Rust mit Renet und Bevy.

Feature-Flags: Weniger in der Produktion ausliefern

Feature-Flags ermöglichen es, bestimmte Funktionen zur Kompilierzeit zu aktivieren oder zu deaktivieren, was das Umschalten von Funktionen basierend auf der Konfiguration erleichtert.

In meinem Voxel-Spiel helfen Feature-Flags bei der Verwaltung von Debugging-Tools wie Drahtgitter-Rendering und Debug-UI. Das Coole an diesen Feature-Flags ist, dass Debug-Code nicht in der Produktion ausgeliefert wird, was die Binärgrösse reduziert und den Release-Build sauber hält.

In Cargo.toml kannst du Feature-Flags wie folgt definieren:

  [features]
egui_layer = []
terrain_visualizer = ["egui_layer"]
renet_visualizer = ["egui_layer"]

Und dann in deinem Code verwenden:

  #[cfg(feature = "egui_layer")] {
    use bevy_inspector_egui::bevy_egui::EguiPlugin;
    app.add_plugins(DefaultPlugins);
    app.add_plugins(EguiPlugin);
}

Der Nachteil ist, dass das Testen aller möglichen Feature-Kombinationen eine Herausforderung darstellt. Mit nur fünf Features hast du bereits 32 verschiedene Konfigurationen zu überprüfen. Aber das ist der Preis der Flexibilität.

Cargo Watch: Automatisierung von Workflows

Während der Entwicklung von rsmc musste ich oft Code neu kompilieren, und das manuelle Neustarten meiner Binärdatei nach jeder Änderung wurde schnell mühsam. Ich wünschte wirklich, ich hätte cargo-watch früher entdeckt.

Installiere es einfach und lass den Watcher seine Arbeit erledigen:

  cargo watch -x 'run --bin client'

Komposition statt Vererbung

Als Ruby-Entwickler, wo Vererbung üblich ist, fühlte sich Rusts Ansatz anders an. Rust hat keine Klassen. Es verwendet Structs und Traits. Dies zwang mich, Komposition statt Vererbung zu verwenden und anders über die Codestruktur nachzudenken.

Hier ist ein Beispiel aus meinem Terrain-Generator:

  pub struct NoiseFunctionParams {
    pub octaves: u32,
    pub height: f64,
    // ...
}

pub struct HeightParams {
    pub noise: NoiseFunctionParams, // Komposition!
    pub splines: Vec<Vec2>
}

Anstatt von einer Basisklasse zu erben, enthält HeightParams eine NoiseFunctionParams-Struktur. Dies hält den Code flexibel und vermeidet tiefe Vererbungshierarchien.

Makros: Code, der Code schreibt

Rusts Makros sind wie Rubys Metaprogrammierung, aber strukturierter und leistungsfähiger. Sie helfen, Boilerplate zu eliminieren, während die Typsicherheit erhalten bleibt.

Hier ist ein Makro, das ich verwendet habe, um Blöcke zu definieren in meinem Projekt:

  macro_rules! add_block {
    ($block_id:expr, $is_solid:expr) => {
        Block {
            id: $block_id,
            is_solid: $is_solid,
        }
    };
}

pub static BLOCKS: [Block; 14] = [
    add_block!(BlockId::Air, false),
    add_block!(BlockId::Grass, true),
    // ...
];

Makros beeinflussen die Laufzeitleistung nicht. Sie sind eine Zero-Cost-Abstraktion, eine grossartige Funktion von Rust.

Rusts Lernkurve: Lohnt sich der Aufwand?

Rust ist nicht die einfachste Sprache, um sie zu erlernen. Der Borrow-Checker braucht Zeit, um verstanden zu werden, und im Vergleich zu Ruby sticht seine Ausführlichkeit hervor. Ruby erreicht mehr mit weniger Symbolen. Während Rusts Explizitheit bei der Wartbarkeit und der Reduzierung versteckter Verhaltensweisen hilft, bedeutet es auch, mehr Boilerplate zu schreiben.

Einer der grössten Nachteile für mich sind die Kompilierzeiten. Sie können frustrierend sein, da Rust strenge Überprüfungen durchführt, aber dies reduziert Laufzeitfehler. Es gibt sogar einen XKCD-Comic darüber.

XKCD 303 - Die #1-Programmiererausrede für legitimes Faulenzen: „Mein Code kompiliert gerade“

Aus meiner Erfahrung hat Rust seine Kompromisse. Das Abfangen vieler Fehler zur Kompilierzeit reduziert den Debugging-Aufwand, aber die strengen Regeln und die Ausführlichkeit machen das Schreiben von neuem Code im Vergleich zu Ruby langsamer. Das sagte, Rusts Sprachserver bieten hervorragende Refactoring-Unterstützung, was die Arbeit mit grösseren Projekten erleichtert.

Für schnelles Prototyping und Iteration sind Skriptsprachen wie Ruby immer noch die bessere Wahl. Wenn jedoch Stabilität, Leistung und langfristige Wartbarkeit wichtig sind, scheint Rust die bessere Wahl zu sein.

Fazit

Rust ist nicht nur eine weitere Sprache. Es verändert, wie du über Programmierung denkst. Es macht dich bewusster in Bezug auf Speicher, Sicherheit und Leistung. Die Lernkurve ist steil, aber wenn du dabei bleibst, lohnt sich der Aufwand.

Ich hoffe, dir hat diese Reise in die Welt von Rust gefallen! Wenn du ein Ruby-Entwickler bist, der auch Rust ausprobiert hat, welche Herausforderungen bist du gestossen? Ich würde mich freuen, deine Gedanken zu hören!