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
rustc
rustdoc
rust-gdb
rust-lldb
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:
$ cargo build
builds the package by invokingrustc
.$ cargo doc
builds the package documentation by invokingrustdoc
.$ cargo run
builds the package and runs the executable.$ cargo test
runs tests for the package.$ cargo bench
runs benchmarks for the package.
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:
name
- The name of the package and the resulting executable.version
- The version number of the package; it follows the Semantic Versioning specification.authors
- All authors that are maintaining the package.edition
- The Rust edition that this package is using.
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