Consider Rust: Introduction to Rust
Understand why Rust exists, what problems it solves, and how to install and run your first Rust program. Learn about memory safety without garbage collection, preventing data races, and Rust's ecosystem.
It's 3 AM. Your phone vibrates. Production is down. The server crashed with a segmentation fault. Somewhere in your codebase, a pointer points to freed memory. Or maybe two threads accessed the same data simultaneously without proper synchronization. The error message tells you nothing useful. You deploy a hotfix, restart the service, and spend the next week trying to reproduce the crash locally.
Memory bugs and data races represent two of the most expensive categories of software defects. They're hard to reproduce, harder to debug, and often manifest only under production load. Languages with manual memory management like C and C++ give you full control but demand perfect discipline. Languages with garbage collection like Java and Go prevent memory corruption but carry runtime overhead and can't guarantee freedom from data races without careful programming.
Rust eliminates both problems at compile time. The compiler enforces memory safety and prevents data races before your code ever runs. If your Rust program compiles, it won't have use-after-free bugs, double-free bugs, or unsynchronized concurrent access to shared data. The cost is learning a different programming model—one where the compiler tracks ownership and borrowing with strict rules.
Where Rust Came From
Mozilla created Rust to build Servo, an experimental browser engine. Browser engines face extreme demands: they must be fast, handle untrusted input, manage complex memory hierarchies, and exploit parallelism across multiple cores. C++ provided performance but accumulated memory bugs and security vulnerabilities. Mozilla needed a language that offered C++ performance with stronger safety guarantees.
Graydon Hoare started Rust in 2006 as a personal project. Mozilla began sponsoring development in 2009. The language reached 1.0 in 2015 after aggressive iteration on its core ideas. The team made backward-incompatible changes during development to get the fundamentals right, particularly the ownership system. Since 1.0, Rust has maintained backward compatibility while expanding its capabilities.
The language doesn't emerge from academic type theory research, though it incorporates ideas from programming language research. It comes from systems programmers solving concrete problems: preventing Firefox crashes, eliminating security vulnerabilities, and making concurrent code reliable.
Memory Safety Without Garbage Collection
Most languages choose between manual memory management or garbage collection. Manual management gives you control and predictable performance but requires careful tracking of allocations and deallocations. Miss a free call and you leak memory. Free something twice and you corrupt memory. Free something while still holding a reference to it and you have undefined behavior.
Garbage collection solves these problems by automatically reclaiming unused memory. The runtime periodically scans for unreachable objects and frees them. You allocate freely without tracking lifetimes. The cost is unpredictable pause times when collection runs, memory overhead for the collector's bookkeeping, and CPU cycles spent on collection instead of useful work.
Rust takes a different approach. The compiler tracks ownership at compile time. Every value has exactly one owner—the variable responsible for freeing it. When ownership transfers to a new variable, the old variable can no longer access the value. When the owner goes out of scope, Rust automatically frees the value. The compiler enforces these rules, so memory corruption becomes impossible. At runtime, there's no garbage collector—just immediate deallocation when variables go out of scope.
This provides C and C++ performance with memory safety guarantees comparable to garbage-collected languages. The tradeoff is compile-time friction. The ownership system has rules you must satisfy. When the compiler rejects your code, you must restructure it to make ownership clear. This front-loads debugging—you fix issues at compile time instead of runtime.
Preventing Data Races
Concurrent programming introduces race conditions when multiple threads access shared data without proper synchronization. One thread reads while another writes, or two threads write simultaneously, producing undefined behavior. Traditional approaches use locks to protect shared data, but locks are error-prone. Forget to acquire a lock and you have a race. Hold locks in the wrong order and you deadlock. Hold locks too long and you kill performance.
Rust prevents data races at compile time through the same ownership system that prevents memory bugs. The rules are simple: you can have many immutable references to data OR one mutable reference, but not both simultaneously. The compiler enforces this. If you try to modify data while immutable references exist, the code won't compile. If you try to create multiple mutable references, the code won't compile.
This makes certain patterns impossible to express incorrectly. You can't accidentally share a mutable vector between threads without synchronization. You can't modify a value while iterating over it. The compiler blocks these operations. When you need shared mutable state across threads, Rust provides types like Mutex and Arc that enforce proper synchronization through the type system.
No Built-In Runtime
Some languages include substantial runtime systems. Java has the JVM with its garbage collector, just-in-time compiler, and class loader. Go includes a runtime with a garbage collector and goroutine scheduler. These runtimes provide services but add weight—startup time, memory overhead, and latency from background operations.
Rust programs compile to native code with minimal runtime support. The compiled binary contains your code and a small runtime that handles stack unwinding on panic and some bookkeeping for dynamic dispatch. There's no garbage collector, no just-in-time compilation, no background threads you didn't create. The binary size reflects only what you use.
This makes Rust suitable for embedded systems, operating system kernels, device drivers, and other contexts where you can't tolerate a heavyweight runtime. It also means Rust programs start instantly and use memory proportional to their workload, with no overhead for runtime services you don't need.
When you need features like async I/O or concurrent task scheduling, you choose a library that implements them. The Tokio runtime provides async execution with work-stealing schedulers and efficient I/O polling. The async-std runtime offers an interface similar to the standard library but with async operations. You pay only for what you use, and different applications can make different choices based on their requirements.
The Ecosystem and Tooling
Cargo is Rust's build tool and package manager. It handles compilation, runs tests, manages dependencies, and builds documentation. Creating a new project requires one command: cargo new project_name. Adding a dependency means editing Cargo.toml to specify the crate name and version. Cargo downloads, compiles, and links dependencies automatically.
The crates.io registry hosts over 130,000 libraries. The ecosystem includes production-grade implementations of common needs: the Tokio async runtime, the Serde serialization framework, the Diesel and SQLx database libraries, the Actix and Axum web frameworks, the Rayon data parallelism library. These crates integrate with Rust's safety guarantees—SQLx validates SQL queries at compile time by connecting to your database during builds.
Rustc, the compiler, provides detailed error messages with suggestions for fixing issues. When the borrow checker rejects your code, the error message explains why and often shows how to restructure your code to satisfy the rules. The compiler includes suggestions for common mistakes and points to documentation for concepts you might not understand.
The standard library is smaller than Go's but comprehensive for systems programming. It provides collections, string handling, file I/O, networking primitives, threading, and synchronization. Unlike Go, it doesn't include an HTTP server—that comes from ecosystem crates. The philosophy favors a stable, conservative standard library with ecosystem innovation in external crates.
Rust integrates deeply with operating systems. Linux kernel development now accepts Rust code. Android's operating system includes Rust components. Cloud-native applications benefit from Rust's performance and small binary sizes. Command-line tools written in Rust start instantly and use minimal resources. WebAssembly support lets Rust run in browsers with near-native performance.
Installation and First Program
Install Rust using rustup, the toolchain manager. It handles installation, updates, and switching between Rust versions.
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
This installs rustc (the compiler), cargo (the build tool), and standard library documentation. Verify the installation:
rustc --version
# rustc 1.75.0 (82e1608df 2023-12-21)
cargo --version
# cargo 1.75.0 (1d8b05cdd 2023-11-20)
Create a new project:
cargo new hello_world
cd hello_world
Cargo generates a project structure:
hello_world/
├── Cargo.toml # Package metadata and dependencies
└── src/
└── main.rs # Source code
Open src/main.rs:
fn main() {
println!("Hello, world!");
}
The fn main() function is the entry point. The println! macro prints to standard output. Macros in Rust end with ! to distinguish them from functions.
Run the program:
cargo run
# Compiling hello_world v0.1.0
# Finished dev [unoptimized + debuginfo] target(s) in 0.50s
# Running `target/debug/hello_world`
# Hello, world!
Cargo compiles the program and executes it. The binary lives at target/debug/hello_world. For optimized builds, use cargo build --release. Release builds take longer to compile but produce faster binaries at target/release/hello_world.
The compilation model differs from interpreted languages. Rust checks types, enforces ownership rules, and generates machine code before execution. Runtime errors that would crash other languages become compile-time errors. The program either compiles correctly or fails with detailed error messages explaining what to fix.
Why This Matters
Traditional systems programming forces a choice between safety and control. High-level languages abstract memory management and provide safety, but you sacrifice performance and predictability. Low-level languages give you control, but you shoulder the burden of correctness. Memory bugs and data races slip through testing and emerge in production.
Rust offers a third option. You get control over memory layout, allocation strategy, and performance characteristics. The compiler ensures safety through ownership and borrowing rules enforced at compile time. Unsafe operations exist but require explicit unsafe blocks that mark where guarantees don't apply. The rest of your codebase enjoys safety without runtime overhead.
The learning curve is steeper than languages with garbage collection. Understanding ownership, borrowing, and lifetimes takes effort. The compiler will reject valid algorithms that you'd implement easily in other languages. You must restructure code to satisfy the borrow checker. This front-loaded difficulty pays dividends when your program compiles and runs without memory corruption or data races.
Modern software runs in environments where failures are expensive. A crashed server costs money. A security vulnerability costs reputation. A data race that manifests only under load costs engineer-weeks to diagnose. Rust eliminates entire categories of bugs before deployment. When your Rust program compiles, you have strong guarantees about its behavior. When you ship code, you ship confidence.
Previous
No previous page
Next
Types, Variables, and Constants
