Rust Introduction

Rust is a systems programming language that gives you the speed and low-level control of C or C++ without sacrificing memory safety. It does this not with a garbage collector, but with a compile-time system called ownership that the compiler enforces before your program ever runs — most memory bugs that plague other low-level languages simply refuse to compile in Rust. This lesson introduces what Rust is, how its compiler thinks differently from other languages, and how to install the toolchain and write your first programs.

Overview: How Rust Thinks Differently

Most languages force a choice between two strategies for managing memory. Languages like C and C++ hand memory management entirely to the programmer: you allocate and free memory yourself, which is fast but leaves the door open to dangling pointers, double frees, and use-after-free bugs. Languages like Java, Python, and JavaScript take the opposite approach: a garbage collector runs in the background at runtime, tracking which memory is still reachable and freeing the rest — safe, but with a memory and CPU cost, and pauses that are hard to predict.

Rust takes a third path. Every value in a Rust program has exactly one owner — the variable responsible for it. When that owner goes out of scope, Rust automatically inserts the code to free the value’s memory, at compile time, with no runtime garbage collector watching over anything. Think of it like a library with a strict single-card checkout system: a book has exactly one card holder at a time, the librarian (the compiler) refuses any checkout that would create a conflict, and when the card holder is done, the book is automatically returned. No one has to patrol the shelves at runtime looking for abandoned books — the rules of checkout make abandonment impossible in the first place.

This compile-time bookkeeping is called the borrow checker, and it is the feature Rust is best known for. It rejects certain programs that would compile fine in C — programs that might read freed memory or mutate data while something else is reading it — before they ever run. The tradeoff is a stricter compiler and a learning curve; the payoff is that whole categories of bugs (segfaults, data races, use-after-free) become compile-time errors instead of production incidents. Ownership and borrowing get a full, dedicated lesson later in this course — for now, just know that this compile-time discipline is the reason Rust code looks the way it does.

Beyond memory safety, Rust is statically and strongly typed, compiles directly to native machine code (no interpreter or virtual machine at runtime), and is designed around zero-cost abstractions — high-level conveniences like iterators and generics that compile down to code as efficient as hand-written loops. It is used for command-line tools, web backends, game engines, embedded systems and operating systems, and WebAssembly, and its package ecosystem (crates.io) is large and actively maintained.

The Rust Toolchain: rustc and Cargo

A Rust installation gives you two tools you’ll use constantly. rustc is the compiler itself — it turns a .rs source file into an executable. In practice, you rarely call rustc directly; instead you use cargo, Rust’s build tool and package manager, which wraps the compiler and handles dependencies, builds, and project layout for you.

The usual workflow is: cargo new my_project creates a new project folder with a Cargo.toml manifest (where dependencies, called crates, are listed) and a src/main.rs file. cargo build compiles the project, cargo run compiles and immediately runs it, and cargo check type-checks and borrow-checks the code without producing a binary — useful for a fast feedback loop while you write. Every example in this lesson is a complete program you could drop into src/main.rs and run with cargo run.

Syntax

Every Rust program starts execution at a function named main. Here is the basic shape, with variable bindings and a macro call:

fn main() {
    let x = 5;          // immutable binding
    let mut y = 10;     // mutable binding
    y += x;

    println!("x = {}, y = {}", x, y);
}
x = 5, y = 15
  • fn main() { ... } — the entry point; every executable Rust program needs exactly one.
  • let — creates a variable binding. Bindings are immutable by default.
  • mut — added after let to explicitly allow a binding to be reassigned.
  • Statements end with a semicolon ;. A block’s final expression without a semicolon is that block’s return value.
  • println!(...) — a macro (note the !) that prints to standard output; {} inside the string are placeholders filled by the arguments that follow.
  • // comment — a line comment; everything after // on that line is ignored by the compiler.
Symbol Meaning
fn Declares a function
let / let mut Declares an immutable / mutable variable binding
{ } Defines a block and a scope
; Ends a statement
! after a name Marks a macro invocation, e.g. println!

Examples

Example 1: Hello, World

fn main() {
    println!("Hello, world!");
}
Hello, world!

This is the smallest complete Rust program. fn main() defines the entry point, and the single statement inside calls the println! macro with a string literal. Compiling this with rustc or running it with cargo run prints the text followed by a newline.

Example 2: Variables and Formatted Output

fn main() {
    let language = "Rust";
    let mut version = 1;
    version += 1;

    println!("Learning {} version {}", language, version);
}
Learning Rust version 2

language is bound to a string literal (a &str, a borrowed string slice) and never changes, so it stays an immutable binding. version is declared with mut because the next line reassigns it with += — without mut this would be a compile error. println! fills its two {} placeholders with language and version in order.

Example 3: Borrowing Instead of Moving

fn describe(name: &str) -> String {
    format!("{} has {} characters", name, name.len())
}

fn main() {
    let name = String::from("Ferris");
    let description = describe(&name);

    println!("{}", description);
    println!("Original name is still usable: {}", name);
}
Ferris has 6 characters
Original name is still usable: Ferris

name owns a heap-allocated String. Passing &name to describe hands over a reference — a temporary, read-only borrow of the data — rather than the data itself. Because describe only borrows name, ownership never moves, and main can still use name after the call returns. If describe had instead taken name: String (by value) and been called as describe(name), ownership would have moved into the function, and the final println! referencing name would fail to compile. This borrow-instead-of-move pattern is the single most common idiom in everyday Rust code.

How It Works Step by Step

When you run cargo build or cargo run, several things happen before you get a binary:

  • Parsing and type checking: rustc parses the source into an abstract syntax tree and checks that every expression’s types line up — a String can’t be passed where an integer is expected, for instance.
  • Borrow checking: the compiler walks through ownership and borrowing for every value: who owns it, how long each reference to it lives, and whether any reference outlives the data it points to or conflicts with another active reference. This is the pass that would reject the moved-then-used mistake shown below.
  • Code generation: once the program passes those checks, Rust hands the verified program to LLVM, which compiles it down to optimized native machine code — the same backend used by Clang for C and C++.
  • No runtime step: there is no interpreter and no garbage collector thread running alongside your program. The binary that comes out is a standalone native executable.

Tracing Example 3 through this pipeline: name is created in main and owns its String data. &name creates an immutable borrow, and the borrow checker confirms that this borrow cannot outlive name and that no conflicting mutable borrow exists at the same time. Inside describe, the parameter name: &str only ever reads through the reference, so when the function returns, there is nothing for it to drop — the original String is still owned by main‘s name binding, which is why the second println! can still use it.

Common Mistakes

Mistake 1: Forgetting mut

Bindings are immutable unless you say otherwise. Trying to reassign one without mut is a compile error, not a runtime one:

fn main() {
    let count = 0;
    count = count + 1; // error[E0384]: cannot assign twice to immutable variable
    println!("{}", count);
}

The fix is to add mut to the binding:

fn main() {
    let mut count = 0;
    count = count + 1;
    println!("{}", count);
}
1

Rust makes immutability the default on purpose: a binding you can’t reassign is one fewer thing to track when reading code, and the compiler — not a code reviewer — catches the mistake instantly.

Mistake 2: Using a Value After It’s Moved

String does not implement Copy, so assigning it to another variable moves ownership rather than duplicating the data. The original binding becomes invalid:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1;
    println!("{}", s1); // error[E0382]: borrow of moved value: `s1`
}

After let s2 = s1;, s1 no longer owns anything — the compiler statically invalidates it so there’s no chance of two owners freeing the same memory. Depending on the intent, the fix is either to clone the data (paying the cost of a real copy) or to borrow instead of move:

fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("{} {}", s1, s2);
}
hello hello

This is the single most common error new Rust programmers hit, and it is also the clearest sign the borrow checker is doing its job: in C++ the equivalent code would compile and might work by accident, or might silently corrupt memory if both variables later tried to free the same heap allocation.

Best Practices

  • Let the compiler’s error messages guide you — Rust’s diagnostics usually name the exact rule violated and often suggest the fix directly.
  • Default to immutable bindings (let) and only add mut when a variable genuinely needs to change; it documents intent for readers.
  • Prefer borrowing (&value) over cloning when a function only needs to read data — cloning is a real allocation and copy, not a free operation.
  • Use cargo fmt to keep formatting consistent and cargo clippy to catch common mistakes and non-idiomatic patterns beyond what the compiler itself checks.
  • Run cargo check frequently while writing code — it runs the same type and borrow checks as cargo build but skips code generation, so it’s much faster.
  • Read error codes like E0382 literally — running rustc --explain E0382 prints a detailed explanation with examples.

Practice Exercises

  • Write a program that declares two mutable integers, swaps their values using a third temporary variable, and prints both before and after the swap.
  • Write a function shout(text: &str) -> String that returns the input in uppercase with an exclamation mark appended (hint: string slices have a .to_uppercase() method), then call it from main with a string literal and print the result.
  • Predict, then verify by reasoning through the rules in this lesson: will the following compile? let a = String::from("hi"); let b = a; println!("{}", a); If not, rewrite it so both a and b can be printed.

Summary

  • Rust is a compiled, statically typed systems language that guarantees memory safety at compile time instead of using a garbage collector.
  • The borrow checker enforces ownership and borrowing rules before your program runs, turning many runtime bugs into compile errors.
  • rustc is the compiler; cargo is the build tool and package manager you’ll use for almost everything day to day.
  • Bindings created with let are immutable by default; add mut to allow reassignment.
  • Assigning a non-Copy value like String moves ownership; the original binding becomes unusable afterward unless you borrow (&) or clone() instead.
  • println! and other macros ending in ! are expanded at compile time, not ordinary function calls.