Introduction to Rust #
Some categories of bugs have haunted the software world for decades: use-after-free, dangling pointers, data races, and buffer overflows. Languages such as C and C++ provide complete control over memory, but leave all responsibility to the programmer. Languages with garbage collectors, such as Java and Go, take a different path: runtime guarantees memory safety, but at the cost of latency and unavoidable overhead. Rust offers a different proposition: memory safety guaranteed by the compiler, without a garbage collector and without runtime overhead. It does not limit what you can do; it makes unsafe code impossible to compile. This article covers the foundations of Rust’s philosophy, how the ownership system works, its ecosystem, and why Rust has become a leading choice in domains previously dominated by C and C++.
Rust’s philosophy and three guarantees #
Rust is built on three mutually reinforcing guarantees: memory safety, thread safety, and zero-cost abstractions. They are not trade-offs. Rust aims to provide all three at once, something previously considered impossible in systems programming languages.
Memory safety without a garbage collector — Rust prevents entire classes of memory bugs (null pointer dereference, use-after-free, double free, and buffer overflow) through a static type system rather than runtime checks. If code is not memory-safe, it will not compile.
Thread safety as a type property — A data race in Rust is not merely “forbidden by convention”; it cannot literally be represented in Rust’s type system. The compiler rejects code that could cause a data race before you run it.
Zero-cost abstractions — High-level Rust abstractions (iterators, closures, and generics) do not produce runtime overhead. An iterator chain written declaratively is compiled into a loop equivalent to one written by hand, with no extra cost for readability.
flowchart TD
A[Three Rust Guarantees] --> B[Memory Safety]
A --> C[Thread Safety]
A --> D[Zero-cost Abstractions]
B --> E[Ownership System]
B --> F[Borrow Checker]
C --> G[Send + Sync Traits]
C --> H[Fearless Concurrency]
D --> I[Monomorphization]
D --> J[Inline & Optimization]
E --> K[Compiler Enforcement]
F --> K
G --> K
H --> K
I --> K
J --> KThe ownership system — the core of Rust #
Ownership is the concept that most clearly distinguishes Rust from other languages. There is no garbage collector and no automatic reference counting, only a set of simple rules enforced by the compiler at compile time.
Three ownership rules #
1. Every value in Rust has one owner.
2. There can be only one owner at a time.
3. When the owner leaves scope, the value is dropped (its memory is freed).
These rules sound simple, but their implications run deep:
fn main() {
// s1 is the owner of this String
let s1 = String::from("halo");
// ANTI-PATTERN: move semantics — s1 cannot be used after this
let s2 = s1;
// println!("{}", s1); // ERROR: value borrowed here after move
// CORRECT: if you need two variables, use clone
let s3 = String::from("dunia");
let s4 = s3.clone();
println!("{} {}", s3, s4); // both are valid
}
Types that implement the Copy trait (integers, floats, bool, char, and tuples of Copy types) are not moved. They are copied implicitly because they are small and fixed in size on the stack.
Borrowing and references #
If ownership transfers possession, borrowing lets you use a value without taking ownership, like borrowing a book without having to buy it.
fn hitung_panjang(s: &String) -> usize { // & = reference, does not take ownership
s.len()
}
fn main() {
let s = String::from("hello");
let panjang = hitung_panjang(&s); // borrows s instead of moving it
println!("'{}' panjangnya {}", s, panjang); // s is still valid here
}
Rust has two kinds of references with strict rules:
| Reference Type | Simultaneous Count | Can Be Modified? |
|---|---|---|
Immutable (&T) | Unlimited | No |
Mutable (&mut T) | Exactly one | Yes |
These rules prevent data races definitively: you cannot have a mutable reference at the same time as any other reference, whether mutable or immutable.
fn main() {
let mut s = String::from("halo");
// ANTI-PATTERN: two mutable references to the same data
// let r1 = &mut s;
// let r2 = &mut s; // ERROR: cannot borrow `s` as mutable more than once
// CORRECT: use a scope to limit the reference's lifetime
{
let r1 = &mut s;
r1.push_str(", dunia");
} // r1 leaves scope here
let r2 = &mut s; // now it is safe
println!("{}", r2);
}
Lifetimes #
A lifetime is an annotation that tells the compiler how long a reference remains valid. Most of the time, the compiler can infer it on its own (lifetime elision). In some cases, especially when a function returns a reference, you need an explicit annotation.
// The compiler does not know whether the return value lives as long as 'x or 'y
// ANTI-PATTERN: no lifetime annotation — ambiguous
// fn terpanjang(x: &str, y: &str) -> &str { ... }
// CORRECT: the 'a lifetime annotation says that the return value lives
// for as long as the shorter-lived of x and y
fn terpanjang<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() {
x
} else {
y
}
}
fn main() {
let s1 = String::from("string panjang");
let hasil;
{
let s2 = String::from("xy");
hasil = terpanjang(s1.as_str(), s2.as_str());
println!("String terpanjang: {}", hasil);
}
}
sequenceDiagram
participant Code as Source Code
participant BC as Borrow Checker
participant Compiler as Compiler
participant Binary as Binary
Code->>BC: Send ownership graph
BC->>BC: Validate ownership rules
BC->>BC: Validate borrowing rules
BC->>BC: Validate lifetimes
alt All rules satisfied
BC->>Compiler: Continue compilation
Compiler->>Binary: Generate binary (without GC overhead)
else Violation found
BC-->>Code: Compile error + detailed explanation
endRust’s history and evolution #
Rust was born from real frustration, not an academic setting, but a practical problem faced by a developer in everyday life.
Graydon Hoare started Rust in 2006 as a personal project after returning to his apartment and finding that his building’s elevator had crashed because of a bug in its control software, which was written in C. He wanted a language that would not allow that class of bug to exist.
| Year | Version / Milestone | Significance |
|---|---|---|
| 2006 | Graydon Hoare’s personal project | Initial motivation: software safety for systems programming |
| 2009 | Mozilla began funding it | Rust would be used to build components of the Firefox browser |
| 2010 | Open source release | The community began to form |
| 2015 | Rust 1.0 | First stable release, with a commitment to backward compatibility |
| 2018 | Rust 2018 Edition | NLL, better borrowing ergonomics, and the beginnings of async |
| 2019 | Rust 1.39 | async/await stabilized; Rust was ready for async programming |
| 2020 | Rust Foundation established | Mozilla reduced its involvement and the community took over; Mozilla, Google, Microsoft, Amazon, and Huawei were founding members |
| 2021 | Rust 2021 Edition | Better ergonomics and a new resolver for Cargo |
| 2022 | Linux kernel adopted Rust | Rust officially became the second language supported by the Linux kernel |
| 2023 | Android and Windows kernels | Google and Microsoft began writing new OS components in Rust |
The Linux kernel’s adoption of Rust in 2022 was historic. For the first time since 1991, Linus Torvalds opened the kernel to a language other than C, and that language was Rust.
stateDiagram-v2
[*] --> Research: 2006-2009
Research --> EarlyDev: 2009-2014
EarlyDev --> Stable: Rust 1.0 (2015)
Stable --> Growing: 2015-2019
Growing --> Mainstream: async/await (2019)
Mainstream --> SystemsAdoption: Linux kernel (2022)
SystemsAdoption --> [*]
Research: Personal project, concept exploration
EarlyDev: Mozilla backing, mature ownership design
Stable: Backward compatibility guaranteed
Growing: crates.io ecosystem expands
Mainstream: Tokio async runtime matures
SystemsAdoption: Rust in the world's major OS kernelsThe ecosystem: Cargo and crates.io #
Cargo is one of the best package managers in programming, not only for Rust but overall. It handles dependency management, compilation, testing, benchmarking, and documentation in one integrated tool.
# Create a new project
cargo new nama-project # binary (executable)
cargo new nama-lib --lib # library
# Build and run
cargo build # debug build
cargo build --release # optimized release build
cargo run # build + run
cargo run --release
# Testing
cargo test # run all tests
cargo test nama_fungsi # run a specific test
cargo test -- --nocapture # show println output in tests
# Utilities
cargo clippy # linter — idiomatic Rust recommendations
cargo fmt # format code according to the style guide
cargo doc --open # generate and open documentation
cargo bench # run benchmarks
The Cargo.toml file declares the project and its dependencies:
[package]
name = "aplikasi-saya"
version = "0.1.0"
edition = "2021"
[dependencies]
tokio = { version = "1", features = ["full"] } # async runtime
serde = { version = "1", features = ["derive"] } # serialization
serde_json = "1"
reqwest = { version = "0.11", features = ["json"] }
anyhow = "1" # ergonomic error handling
tracing = "0.1" # structured logging
[dev-dependencies]
mockall = "0.11" # mocking for testing
criterion = "0.5" # benchmarking
[profile.release]
opt-level = 3
lto = true # link-time optimization
Important crates by category #
| Category | Crate | Use |
|---|---|---|
| Async Runtime | tokio, async-std | Future execution, async I/O |
| Web Framework | axum, actix-web, rocket | HTTP server |
| HTTP Client | reqwest, hyper | HTTP client |
| Serialization | serde, serde_json, bincode | Serialize/deserialize data |
| Database | sqlx, diesel, sea-orm | Database queries |
| Error Handling | anyhow, thiserror | Ergonomic error types |
| Logging | tracing, log, env_logger | Structured logging |
| CLI | clap, structopt | Argument parsing |
| Concurrency | rayon, crossbeam | Parallelism and channels |
| Crypto | ring, rustls, sha2 | Cryptography and TLS |
Modern Rust features #
Error handling with Result and Option #
Rust has no exceptions. Instead, errors are ordinary values represented by the Result<T, E> and Option<T> types. This forces programmers to handle every possible error explicitly; no error can “slip through” without being handled.
use std::num::ParseIntError;
// Result<T, E> — an operation that can fail
fn parse_angka(s: &str) -> Result<i32, ParseIntError> {
s.trim().parse::<i32>()
}
// The ? operator — elegant error propagation
fn hitung_dari_string(a: &str, b: &str) -> Result<i32, ParseIntError> {
let x = parse_angka(a)?; // if there is an error, return it immediately
let y = parse_angka(b)?;
Ok(x + y)
}
fn main() {
match hitung_dari_string("10", "20") {
Ok(hasil) => println!("Hasil: {}", hasil),
Err(e) => println!("Error: {}", e),
}
// Or use if let for simple cases
if let Ok(nilai) = parse_angka("42") {
println!("Parsed: {}", nilai);
}
}
Async/await #
Rust supports asynchronous programming through async/await, which compile into a state machine rather than OS threads, avoiding heavy overhead.
use tokio;
#[tokio::main]
async fn main() {
let hasil = ambil_data("https://api.example.com/data").await;
match hasil {
Ok(data) => println!("Data: {}", data),
Err(e) => eprintln!("Error: {}", e),
}
}
async fn ambil_data(url: &str) -> Result<String, reqwest::Error> {
let response = reqwest::get(url).await?;
let teks = response.text().await?;
Ok(teks)
}
// Concurrency without data races — run several futures at once
async fn ambil_parallel() {
let (r1, r2, r3) = tokio::join!(
ambil_data("https://api.example.com/endpoint1"),
ambil_data("https://api.example.com/endpoint2"),
ambil_data("https://api.example.com/endpoint3"),
);
// all three requests run in parallel, safely and without data races
}
Traits — Rust interfaces #
Traits in Rust define behavior that can be shared among types. Unlike interfaces in Java or Go, a Rust trait can be implemented for an existing type, even a type from another library.
trait Ringkasan {
fn ringkas(&self) -> String;
// A method with a default implementation
fn pratinjau(&self) -> String {
format!("Baca lebih lanjut: {}...", &self.ringkas()[..50.min(self.ringkas().len())])
}
}
struct ArtikelBerita {
judul: String,
penulis: String,
konten: String,
}
struct Tweet {
username: String,
konten: String,
}
impl Ringkasan for ArtikelBerita {
fn ringkas(&self) -> String {
format!("{}, oleh {} - {}", self.judul, self.penulis, self.konten)
}
}
impl Ringkasan for Tweet {
fn ringkas(&self) -> String {
format!("{}: {}", self.username, self.konten)
}
}
// Trait bounds — a function that accepts anything implementing Ringkasan
fn notifikasi(item: &impl Ringkasan) {
println!("Breaking news! {}", item.ringkas());
}
Where Rust is used #
Rust is not a general-purpose language for every situation, but it is particularly strong in certain domains.
Systems programming — Linux kernel components, hardware drivers, the Redox operating system, and modern bootloaders are increasingly being written in Rust. Microsoft is actively writing new Windows components in Rust to replace C/C++ in areas sensitive to memory safety.
WebAssembly — Rust was the first language to receive first-class WebAssembly support. Wasm-bindgen and wasm-pack make it easy to compile Rust to Wasm for browser execution with near-native performance.
Blockchain — Solana, Polkadot, and Near Protocol chose Rust as their primary language. The need for high performance and strict security makes Rust a good fit.
CLI tooling — Many modern developer tools have been rewritten in Rust: ripgrep (a replacement for grep), fd (a replacement for find), bat (a replacement for cat), exa/eza (a replacement for ls), delta (a better git diff), and even zed (a code editor).
Cloud infrastructure — AWS Firecracker (microVMs for Lambda and Fargate), the Cloudflare Workers runtime, and various Cloudflare infrastructure components are written in Rust.
flowchart TD
A[Rust] --> B[Systems Programming]
A --> C[Web & Network]
A --> D[Tooling]
A --> E[Blockchain]
A --> F[Embedded]
B --> B1[Linux Kernel]
B --> B2[Windows Components]
B --> B3[OS: Redox]
C --> C1[Axum / Actix Web]
C --> C2[WebAssembly]
C --> C3[AWS Firecracker]
D --> D1[ripgrep, fd, bat]
D --> D2[Zed Editor]
D --> D3[Cargo, rustfmt]
E --> E1[Solana]
E --> E2[Polkadot]
E --> E3[Near Protocol]
F --> F1[RTOS components]
F --> F2[IoT firmware]When to choose Rust #
Rust is not the best choice for every project. Understanding its trade-offs matters before you begin.
Choose Rust if:
✓ You need C/C++-level performance without the risk of memory bugs
✓ Memory safety is critical (security, reliability)
✓ You are building a concurrent system with many threads
✓ The target is systems programming, embedded, or WebAssembly
✓ The team is willing to invest time in the initial learning curve
✓ You are building CLI tools or infrastructure that needs to run for a long time
Consider an alternative if:
✗ You need rapid prototyping or throw-away code → Python, Ruby
✗ You are building a web application with a large team familiar with JavaScript → Go, Node.js
✗ The learning curve is the main obstacle → Go, Python
✗ A domain-specific library ecosystem matters more → Python (ML/AI), JS (frontend)
✗ You do not yet need systems-level performance → almost any other language is easier
| Criterion | Rust | Go | C++ | Python |
|---|---|---|---|---|
| Performance | ★★★★★ | ★★★★☆ | ★★★★★ | ★★☆☆☆ |
| Memory Safety | ★★★★★ | ★★★★☆ | ★★☆☆☆ | ★★★★☆ |
| Ease of Learning | ★★☆☆☆ | ★★★★☆ | ★★☆☆☆ | ★★★★★ |
| Concurrency | ★★★★★ | ★★★★★ | ★★★☆☆ | ★★☆☆☆ |
| Ecosystem | ★★★★☆ | ★★★★☆ | ★★★★★ | ★★★★★ |
| Build Time | ★★☆☆☆ | ★★★★★ | ★★☆☆☆ | ★★★★★ |
Rust build times, especially for large projects with many dependencies, can be very slow. This is a known trade-off: the compiler does more work (borrow checking, monomorphization, and optimization) to produce safer and faster binaries. Investing insccache(a shared compilation cache) ormold(a modern linker) can help significantly.
FAQ #
Is Rust difficult to learn?
Rust has a steeper learning curve than most modern languages. At first, the borrow checker often feels like “fighting the compiler.” But it is an investment: once you understand ownership, you will write code that is fundamentally correct, not merely code that happens to work.
Can Rust replace C/C++?
For new projects, Rust is a superior alternative in nearly every C/C++ domain. However, C/C++ have much more mature ecosystems and enormous existing codebases. Rust and C/C++ can coexist; Rust has an FFI (Foreign Function Interface) that enables direct interoperability with C libraries.
What is a Rust Edition?
Rust uses an “Edition” system (2015, 2018, 2021) to introduce breaking changes without damaging older projects. All editions can be linked together in one binary, so old and new libraries can be used together without conflicts. It is an elegant way to evolve the language without fragmenting the ecosystem.
Is Rust suitable for web backends?
Yes. Axum (from the Tokio team) and Actix-Web consistently rank near the top of TechEmpower benchmarks. For APIs that need high throughput and low latency, a Rust backend is highly competitive. For ordinary CRUD applications, however, the productivity of Rails or Go may matter more than Rust’s performance.
How long does it take to learn Rust?
For an experienced programmer coming from another language: 2–4 weeks to understand the basic concepts and write code that compiles, and 2–3 months to start feeling comfortable and writing idiomatic code. Main resources include The Rust Book (free and comprehensive), Rustlings (interactive exercises), and Rust by Example.
Summary #
- Ownership is the foundation — Every value has one owner, and when the owner leaves scope, the value is dropped. The compiler guarantees that this mechanism prevents memory leaks and use-after-free.
- Borrowing prevents data races — Rust permits either one mutable reference or many immutable references at a time. This rule makes a data race literally impossible to compile.
- Lifetimes ensure reference validity — The compiler tracks how long each reference is valid and rejects code that stores a reference to data that has already been dropped.
- Zero-cost abstractions — Iterators, closures, generics, and trait objects in Rust produce no runtime overhead. High-level abstractions remain readable while performing like hand-written code.
- Cargo is a complete ecosystem — A package manager, build system, test runner, linter, and documentation generator in one consistent integrated tool.
ResultandOptionreplace exceptions — Errors in Rust are ordinary values that must be handled explicitly. The?operator keeps error propagation ergonomic.- Async/await without overhead — Rust compiles futures into state machines rather than requiring OS threads. Tokio enables massive concurrency with minimal resources.
- Relevant in critical domains — Linux kernel, Windows components, AWS Firecracker, Solana, and Cloudflare: Rust adoption in real-world critical infrastructure continues to grow each year.
Next: Installation →