Writing the Hello World program

In this lesson, we'll write, compile, and run our first Rust program. We’ll also take a brief look at the structure of a Rust program.

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 in C or C++ in many respects. However, unlike in C or C++, 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 the println! 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.

#rust #introduction