Rust: Building Reliable and Performant Software
Rust is quietly spreading as a language of choice for building reliable and performant applications. But what makes it different? Ellis Real, a software engineer on Google's Android Rust team, a core maintainer of Tokio (the de facto async runtime for Rust), and a Rust language team advisor, joins us to discuss the compelling reasons to consider Rust, whether you're coming from TypeScript or C++.
In this conversation, we delve into concepts like ownership, the borrow checker, and the unsafe keyword. We also explore what trips up newcomers to Rust, how the language is governed without a benevolent dictator, and the mechanics of RFCs and "editions." If you want to understand what sets Rust apart and why so many engineers attest that "once it compiles, it works," this episode is for you.
Tokio: An Asynchronous Runtime
Tokio is an asynchronous runtime for Rust, essentially serving as the standard library for asynchronous programming in the language. Similar to how JavaScript in a browser uses an event loop to manage tasks, Tokio manages a queue of runnable items. When using async/await, tasks can pause, allowing other tasks to run on the same thread. Tokio extends this by being multi-threaded, enabling multiple queues to run in parallel.
What Makes Rust Appealing
A significant draw of Rust is the strong feeling that "when it compiles, it works." While not literally bug-free, this sentiment stems from Rust's robust type system. Compared to other languages with type systems, Rust excels in preventing common pitfalls. For instance, Java's null reference, famously dubbed a "billion-dollar mistake," can lead to frequent crashes. Rust, however, forces explicit handling of potential nullability, preventing such errors at compile time.
Rust also eschews exceptions for error handling, instead returning errors as values. This is managed through an enum that can be either a Result or an error. The ? operator simplifies error propagation: if a function returns an error, the ? operator automatically returns that error from the current function. This makes error handling explicit and less prone to being overlooked, unlike exceptions which can be implicitly handled.
Documentation in Rust is also a standout feature. Comments marked with three slashes (///) become documentation comments. Crucially, examples embedded within these comments are automatically compiled and run as tests. This ensures that documentation examples remain synchronized with the actual code, preventing outdated or broken examples.
Furthermore, Rust prevents uninitialized variables, requiring them to be assigned a value before use. Libraries like Serde leverage macros to generate efficient code for deserializing data, such as JSON, ensuring that incoming data conforms to the expected structure and types. The match statement, similar to switch statements in other languages, is exhaustive for enums. If a variant is missed, the compiler will flag it as an error, forcing developers to handle all possibilities or explicitly include a catch-all case. This design philosophy extends to refactoring, where the compiler guides developers to update all necessary code locations after changes.
Rust for TypeScript Engineers
For developers coming from TypeScript, Rust is an excellent fit for backend development, API servers, and other server-side applications. The primary pitch is reliability: Rust aims to minimize bugs, preventing the kind of issues that might cause developers to be woken up in the middle of the night. Beyond the absence of null pointer exceptions and robust error handling, Rust's type system and explicit error management contribute significantly to this reliability.
Moving from C++ to Rust
The pitch for C++ developers is even stronger, primarily due to Rust's memory safety. In C++, small mistakes like off-by-one errors in array indexing can easily become security vulnerabilities. Rust eliminates this entire class of bugs.
Memory Safety
Memory safety ensures that code, no matter how flawed, won't suffer from a specific set of bugs that often lead to security vulnerabilities. These include reading past the bounds of an array or using memory after an object has been deallocated. In the Linux kernel, for example, a vulnerability could arise if an attacker manages to overwrite a task_struct (representing a process) with a crafted object, potentially setting the user ID to zero and gaining root privileges. Rust's memory safety guarantees prevent such exploits.
Garbage Collection Tradeoffs
Rust does not have a garbage collector. Instead, memory is managed through an ownership model where variables are cleaned up when they go out of scope. This contrasts with languages like Java or C#, which use garbage collectors to periodically reclaim unused memory.
The absence of a garbage collector offers several advantages:
- Predictable Performance: Garbage collection can introduce latency spikes, which are unacceptable in real-time systems or embedded environments. Rust's deterministic memory management avoids these unpredictable pauses.
- Low-Level Control: For embedded systems or operating system development, direct control over memory is crucial, which Rust provides.
- Backend Efficiency: Even in backend services, avoiding garbage collection can lead to more consistent response times and reduced latency.
Ownership, References, and Borrowing
Rust's ownership model is a core concept. Each value in Rust has a variable that's its owner. There can only be one owner at a time. When the owner goes out of scope, the value is dropped.
- Move Semantics: When you assign a variable to another (e.g.,
let b = a;), ownership is moved. The original variableabecomes invalid and cannot be used afterward. This prevents double-free errors that can occur in languages without garbage collection. - Reference Counting (
Arc): For scenarios where multiple parts of the program need to share ownership of data, Rust providesArc(Atomically Reference Counted). Cloning anArcincrements a counter. When anArcgoes out of scope, the counter is decremented. When the counter reaches zero, the data is deallocated. This allows for shared ownership without a garbage collector. - Borrowing and the Borrow Checker: Rust also allows you to borrow data without taking ownership. This is done through references. The borrow checker enforces strict rules at compile time to ensure memory safety:
- You can have multiple immutable references (readers) to a piece of data.
- You can have only one mutable reference (writer) at a time.
- Mutable borrows cannot coexist with immutable borrows.
- References must not outlive the data they point to.
"Fighting the borrow checker" is a common experience for newcomers. Often, the solution involves rethinking data structures rather than just tweaking the code. For example, trying to create cyclic data structures (like a book referencing its pages, and pages referencing the book) without proper reference counting can lead to borrow checker errors.
Unsafe in Rust
The unsafe keyword in Rust acts as an "escape hatch." While Rust guarantees memory safety without unsafe, using it allows for operations that the compiler cannot statically verify. Each unsafe operation has a set of rules that the programmer must adhere to.
Common uses of unsafe include:
- Performance Optimizations: Bypassing bounds checks on array accesses (e.g., using
get_unchecked) for performance-critical code. - Interfacing with C Libraries: Calling functions from C libraries that may not be memory-safe.
- Implementing Low-Level Abstractions: Building new data structures or abstractions (like a custom
Vectype) that rely on raw memory manipulation.
When unsafe code is properly encapsulated within safe APIs, it can provide the benefits of low-level control and performance without exposing the underlying unsafety to the rest of the program.
Crates and Cargo
The Rust ecosystem revolves around crates, which are Rust's term for packages. The build tool and package manager for Rust is Cargo. Cargo handles:
- Building Code: Compiling your project with
cargo buildor running it withcargo run. - Dependency Management: Declaring dependencies in
Cargo.tomland downloading them from a central registry. - Testing: Running unit tests and documentation tests with
cargo test. - Documentation Generation: Creating API documentation with
cargo doc.
Unlike package managers like pip which install packages globally or in virtual environments, Cargo's dependencies are typically local to the project. This isolation helps prevent conflicts between different projects.
Linus Torvalds has expressed reservations about Cargo downloading and running code from the internet, preferring the security guarantees of distribution-specific package managers. While the Rust ecosystem has not seen the same level of supply chain attacks as some others (like npm), the risk of malicious crates exists for any internet-connected package manager.
Ecosystem Maturity
The Rust ecosystem is most mature for backend services and command-line tools. While efforts are underway to use Rust for front-end development via WebAssembly, it's generally recommended to pair Rust backends with TypeScript frontends. Rust is also seeing significant adoption in embedded systems and operating system components, including the Linux kernel.
Language Design and Governance
The Rust project is governed by various teams, including the Language Team, Library API Team, and Dev Tools Team. Unlike languages with a single benevolent dictator, Rust's decisions are made through consensus among these teams.
- RFC Process: For significant language changes, Rust uses a Request for Comments (RFC) process. An RFC document outlines the motivation, design, alternatives, and rationale for a proposed feature. This process encourages broad discussion and feedback.
- Final Comment Period (FCP): Once an RFC is approved by the relevant team, it enters a Final Comment Period (FCP), typically two weeks, allowing for final review and objections before acceptance.
- Editions: Rust uses "editions" (e.g., 2015, 2018, 2021, 2024) to introduce breaking changes without breaking existing code. Crates can opt into new editions, allowing them to use new syntax or features while still being compatible with older codebases. This is a key difference from versioning in languages like Python, where migrating from Python 2 to 3 was a significant undertaking.
Getting Paid to Work on Rust
While the Linux kernel has a large number of contributors paid by companies to work on it, the Rust project also sees significant corporate support. Many developers are employed by companies to work on Rust, and the Rust Foundation offers grants to support full-time work, particularly for students and those new to open-source contributions.
Contributing to Rust
For those interested in contributing to Rust, resources include:
- Issue Trackers: Identifying issues on project repositories.
- Rust for Linux Project: A dedicated page with contributing guidelines and links to relevant issue trackers.
- Rustlings: A project that provides small, unfinished Rust code snippets that learners need to complete, offering hands-on practice.
Rust in the Linux Kernel
The integration of Rust into the Linux kernel has evolved significantly. Initially met with skepticism, Rust is now officially recognized as a supported language within the kernel, no longer considered experimental. This shift reflects the growing confidence in Rust's ability to provide memory safety and reliability for critical system components. Government regulations, such as those from the US Department of Defense, are also pushing for the adoption of memory-safe languages, further bolstering Rust's appeal.
AI Use Cases for Rust
AI tools are beginning to find their place in the Rust ecosystem. While still learning how to best leverage them, developers are finding them useful for tasks like generating code snippets, writing benchmarks, and even assisting with code reviews. The strong compile-time guarantees of Rust make it a promising candidate for AI agents, as they can receive more immediate feedback on correctness. The Linux kernel community, for instance, is exploring AI for code reviews, seeing it as an additional safety net rather than a replacement for human oversight.
Learning Rust
The official Rust book is an excellent starting point for learning the language. For more intermediate developers, John Gant's book offers deeper insights. Practical projects are also crucial for proficiency. While AI can help generate code, it's essential to understand the underlying concepts, especially Rust's ownership model and the reasons behind compiler errors, to avoid a false sense of comprehension.
Favorite Rust Features
A particularly exciting feature currently being developed for Rust is "in-place initialization." This allows for constructing values directly where they are needed, preventing them from being moved afterwards, which is crucial for certain low-level programming scenarios like in the Linux kernel.
Book Recommendations
- The Rust Programming Language (The Rust Book): The official and comprehensive guide.
- Programming Rust, 2nd Edition by Jim Blandy, Jason Orendorff, and Leonora Tindall: A highly regarded book for intermediate Rust developers.
- Rustlings: An interactive project for hands-on learning.
Key Takeaways
- Rust prioritizes reliability and performance through its ownership model, borrow checker, and strong type system, aiming to eliminate common programming errors at compile time.
- Memory safety is a core tenet of Rust, preventing entire classes of bugs that often lead to security vulnerabilities, making it a compelling alternative to C++.
- The absence of a garbage collector provides predictable performance and low-level control, suitable for embedded systems, kernels, and high-performance backends.
- Rust's governance model emphasizes consensus through teams and an RFC process, while "editions" allow for language evolution without breaking backward compatibility.
- The ecosystem is mature for backend services and command-line tools, with growing adoption in embedded systems and the Linux kernel.
- AI tools are emerging as helpful aids for Rust development, particularly in code generation, testing, and review, complementing Rust's inherent safety features.