As we learned in our previous lesson, Rust is a compiled language. This means that before we can execute a Rust program, it must first be compiled into machine code, which allows it to run natively on the device.
Every Rust installation includes a built-in compiler to handle this process, which can be accessed via the rustc
command. For example, when I run $ rustc -V
on my machine, it displays the version information of the Rust compiler.
rustc 1.81.0 (eeb90cda1 2024-09-04)
If you do not see an output like this, please refer to this troubleshooting guide.
A normal Rust program ends with .rs
extension. For the “Hello World” program, let’s create a hello_world.rs
file in a directory. It is idiomatic to _
(underscores) in the file name than _
(hyphens). Open this file with your faviourite IDE or code editor and paste the following content.
fn main() {
println!("Hello, world!");
}
Let’s understand the anatomy of this Rust program.
In Rust, there are two main types of programs: binary and library. A binary program is one that runs and produces output, such as printing “Hello, World!” to the screen. A library, on the other hand, contains code that is used by other programs but does not run on its own. The first type is called a binary (or bin
) program, while the second is referred to as a library (or lib
) program. In this lesson, we are writing a binary program because we want to run it.
In the above program, we define a function main
using the fn
keyword. The fn
keyword is a reserved keyword in Rust that is used to define a function. We can name a function anything we want, and we will discuss this in detail in the functions lessons. However, main
is a special function in Rust. It is automatically executed when the program runs. It doesn’t have any parameters (meaning it doesn’t accept any arguments). We don’t need to return anything; in which case, it implicitly returns ()
(equivalent to void
, undefined
or null
in other languages). Alternatively, we can return a Result
value, but we’ll cover that in upcoming lessons.
Rust’s
main
function is quite similar to the main function inC
orC++
in many respects. However, unlike inC
orC++
, we are not required to return an integer value for the status code.
So in a nutshell, when we run the hello_world.rs
program, the main
function is executed, and this serves as the entry point of our program. This means that whatever we want to accomplish with our program must be done inside the body of the main
function. For this lesson, we want to print “Hello, World!” to the screen. We achieve that by placing the println!("Hello, World!")
code inside the {}
(curly brackets) that define the start and end of the function body.
At first glance, the println!
might look like a regular Rust function since we are calling it with ("Hello, World!")
as if "Hello, World!"
is an argument. However, println!
is actually a macro. We’ll dive deeper into macros in a separate lesson, but for now, you can think of a macro as a piece of code that gets expanded during compilation. The ("Hello, World!")
part is inserted into this expanded code at compile time, and the result is that "Hello, World!"
is printed to the screen at runtime
Internally, the macro uses the
std::io::stdout
APIs to print to the standard output. For now, you don’t need to worry about the details of how theprintln!
macro works internally — you can simply treat it like a function.
A string in Rust is defined using ""
(double quotes). Unlike some other languages, '
(single quotes) in Rust are used to define a single character value, such as 'a'
. There are other ways to define string values in Rust, but they require more explanation and will be covered in a dedicated lesson.
Unlike some languages like Kotlin or Go, where the use of semicolons is optional, semicolons are mandatory in Rust because they can change the behavior of the program. If a line or piece of Rust code ends with a ;
, it is considered a statement. Without the ;
, it becomes an expression that evaluate to a value and if present at the end of the function body, will be returned implicitely. We will learn more about this in the “Functions” lesson.
We place the ;
at the end of println!("Hello, World!")
because it is a complete statement — it performs an action (printing to the screen) but does not return a value that we need to use. In Rust, a statement is an instruction that does something but does not evaluate to a value, whereas expressions produce a value. Since println!
is a macro that performs an action and does not return a value, it must end with a semicolon to indicate the end of the statement.
What we need to worry about is how to run it. What we know is before we run it, we need to compile it. We do that by using the rustc <filepath>
command.
$ rustc hello_world.rs
This command compiles the Rust program inside hello_world.rs
and outputs a hello_world
file that contains the compiled machine code on a macOS machine (which I am using). On a Windows machine, you would get main.exe
and main.pdb
files. To run the compiled program, you should execute ./hello_world
on macOS or ./hello_world.exe
on Windows from your terminal. This should print "Hello, World!"
to the terminal (standard output or STDOUT).
$ ./hello_world
Hello, world!
With rustc
, we can also cross-compile code. For example, we can compile a Rust program on a macOS machine to run on a Windows machine. This is done by providing a target architecture name with the --target
flag, such as rustc --target=x86_64-pc-windows-msvc hello_world.rs
. We can also specify an optimization level to the Rust compiler using the -C
flag to create a more optimized binary with excellent runtime performance, though at the cost of longer compilation time. For example, rustc -C opt-level=3 hello_world.rs
produces a highly optimized binary. These are more advanced concepts, which we will cover in the “Releases” lesson.
Managing these tasks, along with source code, can be challenging using just rustc
. This is where Cargo comes in. Cargo is Rust’s package manager and build system, simplifying dependency management, code compilation, running tests, and building projects efficiently. It is included with the Rust installation, so there’s no need to install it separately. In the next lesson, we’ll explore the magic of Cargo and see how it makes our development process much easier.