philipp's blog

hecto, Chapter 1: Setup

🏗️ Construction Notice

The old (complete, but outdated) version of hecto is available here. You are looking at the 2024 version of hecto, which is still unfinished. You can follow the progress of the rewrite here. Once it’s done, this notice will be deleted.


Table of Contents

Chapter 1: Setup

philipp's blog Ahh, step 1. Don't you love a fresh start on a blank slate? And then selecting that singular brick onto which you will build your entire palatial estate?

Unfortunately, when you're building a computer program, step 1 can get... complicated. And frustrating. You have to make sure your environment is set up for the programming language you're using, and you have to figure out how to compile and run your program in that environment.

Fortunately, the development environment that Rust comes with does most of the things for us, so you don’t need anything besides a text editor1 and rust. This tutorial was written on a Mac, though it should work on Windows and Linux as well.

How to install Rust

In order to install Rust, we’re going to use rustup, which manages installed Rust versions and associated tools. Rust comes with a fantastic, free book: The Rust Programming Language. I will link to this book often for more in-depth knowledge, whenever I think the book explains it better than I do, To install rustup, just follow Chapter 1.1: Installation for your platform. At the time of writing, it works like this on Mac: Open a terminal (find it by typing “Terminal” into Spotlight) and enter the following command:

curl --proto '=https' --tlsv1.2 https://sh.rustup.rs -sSf | sh

The installer greets you with some options, and offers to just go with the default by hitting Enter. The installation ends with the following encouraging words:

Rust is installed now. Great!

Restart your terminal, and then type the following:

rustc --version

You should see some information about the version you’ve installed. If not, head over to the Installation guide for troubleshooting.

By the way, we’ll use the terminal a lot in this tutorial, so just keep it open.

The main() function

Create a new file named hecto.rsand give it a main() function.(hecto is the name of the text editor we’re building).

fn main() {
    return;
}

In Rust, you have to put all your executable code inside functions. The main() function in Rust is special. It is the default starting point when you run your program. When you return from the main() function, the program exits and passes control back to the operating system2 .

Rust is a compiled language. That means we need to run our program through a Rust compiler to turn it into an executable file. We then run that executable like we would run any other program on the command line.

To compile hecto.rs, run rustc hecto.rs in your terminal. If no errors occur, this will produce an executable named hecto. To run hecto, type ./hecto and press Enter. The program doesn’t print any output3.

Compiling with cargo

rustc is all we theoretically need to build hecto, but Rust comes with some convenient things that will make development easier, and as you will see later, safer.

Rust comes with a program called cargo. If you are used to the JavaScript ecosystem, then cargo can best be described as rust’s equivalent to npm. It helps you manage your dependencies, compile the code and other things. With Rust being installed correctly, you can type the following command to invoke cargo:

cargo --version

We’ll use this program to get started with hecto. Delete your previous hecto.rs and the executable hecto again by typing rm hecto.rs hecto.

In a directory where you’d like hecto to come to life, type the following:

cargo init hecto --vcs none

Using --vcs none part ensures that you init hecto without git support. If you want to use git in this project, omit this flag (but then you’d need to have git installed)

cargo claims success by telling you the following:

 Created binary (application) package

It created a new folder called hecto, populated with some files. We will go through most of them in a bit, the most interesting one is a file called main.rs in a folder called src.

Let’s look at that one. When you open it, you’ll find that it already contains a main(). Just by looking at it, we can infer that this program follows the ritual of the ancients by printing out "Hello, World!" and exiting.

To use cargo to compile this program, make sure you are in the hecto folder (Run cd hecto after cargo init hecto), and then run cargo build in your shell. cargo is not silent as rustcwas and will produce output that looks a bit like this:

   Compiling hecto v0.1.0 (/Users/pflenker/repositories/hecto)
    Finished dev [unoptimized + debuginfo] target(s) in 12.36s

Running this command adds a few more files (which we’ll look at in a second) and puts an executable called hecto into the folder target/debug. Let’s execute ./target/debug/hecto to convince ourselves that this program does, indeed, print out Hello, World! and then exit.

Understanding cargo’s Extra Files (and Features)

Our hecto directory now contains a whole bunch of things (some of them hidden, the specifics might look different on your machine):

  1. A folder called src, containing the main.rs.
  2. Two files named Cargo.toml and Cargo.lock
  3. A folder called target with some hidden files in it and a folder called debug. That folder contains more files and folders and our executable, hecto. What are all these extra things good for? Let’s learn.

The src Folder

The src folder is where all our source files will live. Right now, we only have one. Soon, there will be more.

Cargo.toml and Cargo.lock

This file follows a config format called TOML and is used to tell the compiler a few things about the code it’s supposed to compile. We take a closer look at the default one a few paragraphs down..

cargo also does dependency management for us, and the Cargo.toml will hold these dependencies for us. The Cargo.lock is part of that dependency management and ensures that dependencies stay consistent in different environments. No need to worry about that now - we will deep dive into that in the next chapter.

Build Targets

Let’s take a look at target and its contents. cargo supports multiple so-called build targets. The one that is used as the default is debug, meaning that the final executable is mainly targeted at us, the developers, and not at actual, real end customers.

Another valid target would be release, which is the opposite: Something to put into the hands of the customers, not only for developers.

To build a release version, run:

cargo build --release

Once you do, you will notice that another folder appeared in the target folder, aptly named release.

Wait a minute, what does it mean, “the executable is targeted at someone”, doesn’t it only print “Hello World”, regardless of who uses it?

Well, kind of.
First of all, the Rust compiler tries to be very accommodating to developers and end users, therefore it treats debug and release builds differently. We will encounter a concrete example later, but there are several types of programming errors where Rust assumes that you really, really did not do this on purpose. If a debug build encounters this kind of error, the program crashes, which is the most extreme thing Rust can to tell you just how wrong you’ve been. But Rust does have a way to recover from these errors and can continue. With wrong results though, but it can continue, and assuming that for an end user, a wrong result is bad, but a crash is worse, the same error would not crash the release build.

Secondly, the compiler can perform various optimisations under the hood to make the result faster. But these optimisations take time and make compilation slower. Who compiles all the time? Developers. Who compiles never? Users. Therefore, debug builds disable the optimisations to prioritise speed in development, and release builds enable them to prioritise speed in execution.

Lastly, the executable does contain code other than just “Hello, World”, and Rust adds information to the debug build which will help you debug any issues quicker. This information isn’t needed for customers, therefore it can be excluded from the release build.

cargo also tells us as much during compilation, the final line for a debug release reads like this:

Finished dev [unoptimized + debuginfo] target(s) in 0.04s

This tells us that the build is not optimised and contains debug info.

The release build, on the other hand, ends its compilation with:

Finished release [optimized] target(s) in 0.04s

This tells us that no debug info is contained, and that it’s an optimised build.

Let’s take a closer look at the debug info, but let’s not mess with our new and already beloved text editor code. Instead, head over to the Rust Playground, which helpfully already contains our hello world function. On the top left, you see some buttons Debug, then Stable, and then .... Click these 3 dots and enable backtrace.

Then change the code in the playground as follows:

fn main() {
   panic!("Hello, World");
}

panic! triggers a crash, plain and simple. Click on “Run” to run it. Then click on “Debug”, change it to “Release”, and run it again.

You will notice that therelease build generates the following output:

     Running `target/release/playground`
thread 'main' panicked at src/main.rs:2:4:
Hello, World
stack backtrace:
   0: rust_begin_unwind
   1: core::panicking::panic_fmt
   2: playground::main

The debug output looks like this:

     Running `target/debug/playground`
thread 'main' panicked at src/main.rs:2:4:
Hello, World
stack backtrace:
   0: rust_begin_unwind
             at /rustc/7cf61ebde7b22796c69757901dd346d0fe70bd97/library/std/src/panicking.rs:647:5
   1: core::panicking::panic_fmt
             at /rustc/7cf61ebde7b22796c69757901dd346d0fe70bd97/library/core/src/panicking.rs:72:14
   2: playground::main
             at ./src/main.rs:2:4
   3: core::ops::function::FnOnce::call_once
             at /rustc/7cf61ebde7b22796c69757901dd346d0fe70bd97/library/core/src/ops/function.rs:250:5

Without understanding stack traces yet, you can immediately see that the debug output contains more information than the release versions. Understanding the call stack, so the order in which functions have called other functions and then some more until one of them crashed, can be tremendously useful.

Build Artefacts

So that explains the folders, but what about the stuff in there that is not hecto? Let’s investigate something to further our understanding.

Back in the terminal, run the following commands in order:

cargo clean
cargo build
cargo build

(Yes, we run cargo build two times)

The first command cleans the target directory. The second command runs as before. During third one, though, cargo does not output a line starting with Compiling, as it did before. That is because cargo can tell that the current version of main.rs has already been compiled. If the main.rs was not modified since the last compilation, then cargo doesn’t bother running the compilation again. If main.rs was changed, then cargo re-compiles main.rs. Once your codebase grows, this will become more useful, as most of the components shouldn’t need to be recompiled over and over when you’re only making changes to one component’s source code.

All (okay: most) of the extra files in the target directory are build artefacts which enable cargo to do subsequent compilations more quickly. Find more on this in  cargo’s documentation.

Compiling and Running

Since it's very common that you want to compile and run your program, cargo combines both steps with the command cargo run.

Try changing the return value in main.rs to a string other than Hello, World. Then run cargo run, and you should see it compile. Check the result to see if you get the string you changed it to. Then change it back to Hello, World, recompile, and make sure it's back to returning Hello, World.

Code Review

Let’s take a look together at the code.

Open this step on GitHub

If you follow the link above, you’ll see an annotated commit. It shows you all the changes that have happened in the code between the previous step and the current one, with some comments directly on the line with the relevant code.

In this case, I am describing the contents of the Cargo.toml in that commit.

Working with commits like this comes with a lot of upsides:

I will link to a lot of additional material throughout this tutorial. Here’s a convention I will stick to: Information crucial to understanding the tutorial will be either on the GitHub commits or directly here in the text. Any other link that is not clearly identifiable as a step on GitHub contains optional information that you can skip if you’re not interested.

Wrap-up and outlook

In this chapter, we’ve installed Rust, initialised a bare-bone project and familiarised ourselves with it. We now know how to compile code by hand and with cargo and have a good initial understanding of cargo and the files it produces. Most importantly, we can build stuff and clean up behind us if need be. We also met the Rust Playground, which will sure come in handy once we want to investigate new things in isolation before we put them into action.

In Chapter 2, we’re going to build a program which reads user input, prints it on the screen and exits on pressing q. That alone will hold a ton of beginner’s learnings for us.

This post was last updated 1 week ago.

  1. At one point, you’ll be able to edit hectowith hecto, so you won’t even need a text editor any more.

  2. Rust supports asynchronous code, which we won’t cover in the tutorial. In this case, it’s perfectly possible for main to end but not pass control back to the Operating System yet.

  3. Not sure about you, but creating an actual, real world, grown-up executable was, and somehow still is, a magical moment for me.

#hecto