Daniel Drywa

Game 0 - Part 1: Project Setup

Using Cargo to generate the project

The Rust programming language

The best description of Rust can be found in its Wikipedia entry:

Rust is a multi-paradigm systems programming language focused on safety, especially safe concurrency. Rust is syntactically similar to C++, but is designed to provide better memory safety while maintaining high performance.

What I like the most about Rust is that it is a high-level programming language on a similar level as C, but with a lot of modern concepts. However, I am fairly new to Rust, and as such, don't have much experience with it. So the best way to learn Rust is to just use it for any new project that I am starting, such as this video game.

After installing Rust via rustup, I have to take no further actions to be able to use it. The Rust Programming Language comes bundled with a bunch of executables which I can find in my home folder under ~/.cargo/bin/. The most important ones are the following:

cargo is the Rust package manager. It is able to resolve and download package dependencies; compile packages; distribute packages.

rustc is the compiler that transforms Rust code into object code, which is then passed to the system linker that creates the executable.

rustdoc is the documentation compiler for Rust projects. It is able to generate HTML documentation by processing comments of code files.

rust-gdb and rust-lldb are scripts that start their respective debuggers (GDB and LLDB) with specific flags to work with Rust executables.

The Rust package manager

In theory, everything I need to generate a executable out of Rust code is the compiler. Executing $ rustc main.rs for example would compile the code located within main.rs and generate a main executable. For small projects this approach might be feasible, but as projects grow and start relying on other libraries, it becomes too much of a hassle. I probably would have to write some form of build script to handle all necessary tasks.

This is where the Rust package manager Cargo comes into play. Cargo allows Rust packages to declare various dependencies and package descriptions through metadata files; it downloads and builds package dependencies; and it invokes rustc and other build tools with the correct parameters.

Here are some examples of what the Cargo executable is able to do:

Creating the package

To create a Rust package with Cargo I have to execute $ cargo new game0 which will give me the following output:

Created binary (application) `game0` package

Cargo has now generated the folder game0 with a couple of files in it which I can list by executing $ tree game0/:

game0/
├── Cargo.toml
└── src
    └── main.rs

1 directory, 2 files

Cargo.toml is the package manifest file that contains all the metadata that Cargo needs to build the package. The manifest is written in the TOML configuration file format which is inspired by the INI file syntax. TOML stands for Tom's Obvious, Minimal Language. The contents of my manifest are the following:

[package]
name = "game0"
version = "0.1.0"
authors = ["Daniel Drywa <daniel@drywa.me>"]
edition = "2018"

[dependencies]

The manifest file contains one or more sections. The first section is [package] that describes the basic properties of the package. In my case it describes the following:

The next section in the manifest is [dependencies]. This section is currently empty as this package doesn't have any dependencies yet. If I wanted to use some other Cargo package within my application, I would have to add it under this section.

The second file Cargo generated is src/main.rs which is the main code file for this package. By default it contains the following code:

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

This program prints the line Hello, world! to the terminal which I can verify by executing $ cargo run from within the game0 folder. This will build the package and run the generated executable, which prints the following output:

   Compiling game0 v0.1.0 (/home/daniel/Projects/game0)
    Finished dev [unoptimized + debuginfo] target(s) in 0.27s
     Running `target/debug/game0`
Hello, world!

The first three lines of output are generated by Cargo to showcase the steps it was undertaking to build and run the executable. The fourth and last line is the actual output of my program. In this case "Hello, world!". The generated executable can be found under target/debug/ with the name game0.

If I am executing $ cargo run a second time, I get the following output:

    Finished dev [unoptimized + debuginfo] target(s) in 0.01s
     Running `target/debug/game0`
Hello, world!

Cargo detected that there have been no changes made since the last build and was therefore able to skip the build step. It ran the already generated executable from target/debug/. If I were to make a change in the src/main.rs file, Cargo would build the executable again to include the change.

If I want to force a rebuild of the package, I could execute $ cargo clean which would delete the target folder. If I then would execute $ cargo run, Cargo would have to build the package again before being able to run it.

I can also build a package without running it by executing $ cargo build.

Cargo and rustc

Cargo executes rustc under the hood when building a package. To see what kind of arguments Cargo passes to rustc I have to set the output to verbose. This can be done by executing $ cargo build --verbose which prints the following output:

   Compiling game0 v0.1.0 (/home/daniel/Projects/game0)
     Running `rustc --edition=2018 --crate-name game0 src/main.rs --color always --crate-type bin --emit=dep-info,link -C debuginfo=2 -C metadata=80a70b8400dcd853 -C extra-filename=-80a70b8400dcd853 --out-dir /home/daniel/Projects/game0/target/debug/deps -C incremental=/home/daniel/Projects/game0/target/debug/incremental -L dependency=/home/daniel/Projects/game0/target/debug/deps`
    Finished dev [unoptimized + debuginfo] target(s) in 0.26s

The second line which starts with Running is showing the exact arguments that are being passed to rustc.

--edition=2018 specifies which edition of the compiler to use when compiling the code. Cargo uses the newest Rust edition for new packages by default. In this case 2018.

--crate-name game0 sets the name of the executable as specified in the Cargo.toml.

src/main.rs is the code file to compile. I only have a single code file for now.

--color always specifies colourized text output in the terminal for any output messages during compilation.

--crate-type bin instructs the compiler that we want to build a binary executable.

--emit=dep-info,link specifies the type of output to emit, besides the executable. In this case, dependency and link information which comes in form of a .d file.

-C debuginfo=2 includes full debug information into my executable.

-C metadata=80a70b8400dcd853 specifies the additional data (80a70b8400dcd853) that will be used for symbol mangling. This guarantees a unique identifier for package symbols during compile time so they won't conflict with symbols of other packages.

-C extra-filename=-80a70b8400dcd853 appends -80a70b8400dcd853 to the output files of this package so they won't conflict with output from other packages. The same identifier as in symbol mangling is used.

--out-dir /home/daniel/Projects/game0/target/debug/deps puts the compiler output into target/debug/deps.

-C incremental=/home/daniel/Projects/game0/target/debug/incremental sets the working folder for incremental compilation.

-L dependency=/home/daniel/Projects/game0/target/debug/deps specifies the path to look for dependencies. All packages that my executable depends on will be put into this folder.

To see all the files Cargo and rustc are generating to build my package, I can execute $ tree target/debug/ which gives me the following output:

target/debug/
├── build
├── deps
│   ├── game0-80a70b8400dcd853
│   └── game0-80a70b8400dcd853.d
├── examples
├── game0
├── game0.d
├── incremental
│   └── game0-mgzi84iu7iwc
│       ├── s-f9kxt1jvam-mtuitp-5chilv1l8vwo
│       │   ├── 2hf9ricv7n0vn9gs.o
│       │   ├── 2o55hkw0l9u9glaa.o
│       │   ├── 3nuzgffclybznqfm.o
│       │   ├── 4re60fsia12celap.o
│       │   ├── 4rkjh7v5d21xy9zc.o
│       │   ├── dep-graph.bin
│       │   ├── query-cache.bin
│       │   ├── v0f05eczua95yqu.o
│       │   └── work-products.bin
│       └── s-f9kxt1jvam-mtuitp.lock
└── native

I can recognize the game0-80a70b8400dcd853 executable with the -80a70b8400dcd853 suffix specified by -C extra-filename, as well as the dependency and link info .d file. The incremental folder contains a bunch of files needed for incremental compilation, but going into detail here is out of scope of this journal. The final resulting executable can be seen in target/debug/game0. Instead of executing $ cargo run, I could directly execute game0 from the target/debug folder. But using Cargo is more convenient.

Cargo is doing a lot of work under the covers to build my package and is taking care of passing the right arguments to the Rust tools. I am happy that I don't have to write and maintain my own build script to set all these options.

Summary

The Rust programming language comes with a bunch of useful tools. The Rust package manager Cargo is being used to create, build, and publish packages. Cargo is executing other Rust tools under the covers with the right arguments, so I don't need to worry about them.

Right now, I have a executable file that prints "Hello, world!" to the terminal. But what exactly does it mean to print to the terminal? I will talk about this next time.

Related Posts

Resources


22 February 2019 (Updated: 31 March 2019)Talk to me