Why this matters
Everything you will write in Rust ends up as a file of numbers that your CPU executes. Knowing what happens between the text you type and the program that runs explains a great deal: why Rust catches some bugs before the program ever starts, why compiling takes a few seconds, why the finished program is fast, and why the compiler's messages are worth reading closely. Rust leans on its compiler more than most languages, so a clear picture of the compiler pays off in every lesson that follows.
What a program is
A CPU understands only a small set of very simple instructions, each one a number: load a value from memory into a register, add two registers, compare, jump to another instruction if the comparison came out a certain way, store a register back to memory. A program, at the lowest level, is a long sequence of such instructions plus the data they work on. That form is called machine code. It is specific to a CPU family: x86-64 machine code does not run on an ARM chip.
Nobody writes machine code by hand any more. We write source code in a language designed for people, and a tool translates it. There are two broad ways to do the translation:
- An interpreter (Python, bash) reads your source while the program runs and carries out each piece as it goes. The interpreter must be present every time the program runs.
- A compiler (Rust, C, Go) translates the entire program ahead of time into machine code and writes an executable file. Running that file needs no compiler and no source.
Rust is compiled. The compiler is called rustc.
What rustc does, step by step
When you run rustc main.rs, a lot happens in a couple of seconds:
main.rs (text)
|
| 1. parse: characters -> tokens -> syntax tree
v
syntax tree ---- 2. expand macros (println! becomes ordinary code)
|
| 3. resolve names, check types, check ownership and borrowing
v
checked program -- errors stop here, with a message, and no executable is written
|
| 4. lower to an intermediate form, optimize (LLVM)
v
machine code ----- 5. link with the standard library and the C runtime
|
v
main (executable file on disk)
Step 3 is where Rust is unusual. Besides checking that types line up (you cannot add a number to a piece of text), rustc checks rules about who owns each piece of memory and who may change it. Whole families of bugs that crash C programs or corrupt their data at run time are, in Rust, compile errors. The price is that the compiler refuses programs more often, and learning Rust is partly learning to read its refusals. The upside is that those refusals are some of the best error messages of any compiler.
Step 5, linking, glues your code together with the parts of the standard library you used (the code that actually writes text to the terminal, for example) and produces a file the operating system knows how to load. On Linux that file format is called ELF.
Your first program
Here is the smallest useful Rust program:
fn main() {
println!("Hello, world!");
}
Piece by piece:
fndeclares a function, a named block of code.mainis special: when the operating system starts your program, execution begins atmain. Every Rust program has exactly one.()is the list of parametersmaintakes: none.{ ... }is the function's body, the code that runs.println!prints text followed by a newline. The!means it is a macro, not a plain function; more on that below."Hello, world!"is a string literal: text written directly in the source, between double quotes.;ends the statement.
Compile and run it in a terminal:
$ rustc main.rs
$ ./main
Hello, world!
rustc main.rs writes an executable called main next to the source. ./main asks the shell to run the file named main in the current directory. The exercises in this course build your code with a few extra options, rustc --edition 2021 -g -o prog main.rs: --edition 2021 selects the current version of the language rules, -g keeps debugging information, and -o prog names the output file prog.
Looking at the result
The executable is an ordinary file, and you can inspect it:
$ file prog
prog: ELF 64-bit LSB pie executable, x86-64, version 1 (SYSV), dynamically linked, ...
$ ls -l prog
-rwxr-xr-x 1 learner learner 11390376 Nov 1 14:06 prog
Eleven megabytes for one line of output looks absurd. Almost all of it is debugging information for the standard library; the machine code for your main is a few dozen bytes. A release build with symbols stripped is a few hundred kilobytes. You do not need to care about size now, but it is a good reminder that the file contains far more than your own code.
Notice what is not needed to run prog: not main.rs, not rustc. You could copy prog to another Linux x86-64 machine without Rust installed and it would run. That is the practical meaning of "compiled ahead of time".
Statements run in order
A function body is a sequence of statements, executed top to bottom:
fn main() {
println!("one");
println!("two");
println!("three");
}
one
two
three
The order on the page is the order of execution. This sounds too obvious to say, but the programs in later lessons will jump around with loops, function calls and conditions, and "which line runs next?" is the question you will ask most while reading code.
Why println! is a macro
Functions in Rust take a fixed number of arguments with fixed types. println! takes a format string followed by any number of values of any printable type, and it checks at compile time that the string and the values match:
fn main() {
let apples = 3;
println!("I have {} apples", apples);
}
I have 3 apples
The compiler expands the macro into ordinary code that formats apples and writes the result. If you wrote println!("I have {} apples") with no value for the {}, the program would not compile, because the check happens during expansion. You will see more of this in the lesson on formatting; for now, read name!(...) as "a macro call: code that writes code".
Common mistakes
Forgetting a semicolon. This is the error you will meet most often at first:
fn main() {
println!("Hello, world!")
println!("Bye");
}
error: expected `;`, found `println`
--> main.rs:2:30
|
2 | println!("Hello, world!")
| ^ help: add `;` here
3 | println!("Bye");
| ------- unexpected token
The message names the file, line and column (main.rs:2:30), draws the line, and puts a caret exactly where the semicolon should go. Most rustc messages have this shape; the next lessons look at them in detail.
Misspelling a name. Write printline! instead of println! and rustc tells you so, and even suggests the right name:
error: cannot find macro `printline` in this scope
--> main.rs:2:5
|
2 | printline!("Hello");
| ^^^^^^^^^ help: a macro with a similar name exists: `println`
Leaving out the !. println("Hi"); asks for a function called println, which does not exist, so you get cannot find function println in this scope. The fix is the !.
Running the old executable. If compiling fails, the old prog (from the last successful build) is still on disk. Running it shows the old behavior and is very confusing. Always check that the build succeeded before you trust what the program prints.
Recap
- A CPU executes machine code: numbered instructions specific to its architecture.
- rustc compiles a whole Rust program ahead of time into an executable; running it needs neither the source nor the compiler.
- rustc parses, expands macros, checks types and ownership, optimizes, and links. Errors stop the build before any executable is written.
- Every program starts at
fn main(). Statements in a body run top to bottom and end with;. name!(...)is a macro call.println!prints a formatted line and checks its arguments at compile time.
Going further: after a successful build, try objdump -d prog | less in the sandbox shell and search for main to see real machine code (it will look like noise for now, and that is the point). rustc --help lists the compiler's options.