Hello, everyone; after receiving a lot of interest in a recent tweet, I’m starting this blog to share my experience with the Rust programming language. I figured I might as well start with the absolute basics—getting Rust installed and writing and building your first project. If you haven’t already read it, I encourage you to start with the Rust Book. Everything I will share in this post is available there, so skim it, refer to it, or read it thoroughly. I hope what I write here can also be helpful, as I know a different approach and set of examples can sometimes be beneficial in understanding a topic.
In this tutorial, we go over the following:
- Setting up your IDE
- Installing Rust
- Getting started with Cargo
- Compiling your program for production
If you have any feedback or prefer we go over anything different, please let me know on Twitter at @nbrempel!
Setting Up Your IDE
There are many code editors, and I’m sure many of these tools are productive environments to work in. However, if you’re unsure what to use, I recommend Visual Studio Code and the Rust Analyzer extension. I use this as it has many helpful tools—especially when understanding Rust’s ownership concept.
Installing Rust
The first step to working in Rust is installing it on your machine. The best way to do this is to use rustup, an easy-to-use installer. Install the tool on your machine using the following command:
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
Follow the instructions in this prompt to install everything you need to start working in Rust. Once it’s installed, you can easily update to the latest version of Rust when it’s available by running rustup update.
Working with Cargo
Cargo is Rust’s build tool and package manager. It can manage and publish crates (which are what Rust’s external libraries and dependencies are called), create new Rust components, and compile your project.
Starting a new project
Using Cargo to create a new Rust project is straightforward. The following command will create a new project called hello-world:
> cargo new hello-world
Created binary (application) `hello-world` package
If you already have an existing directory you want to you, you can use cargo init:
> mkdir existing-directory
> cd existing-directory/
> cargo init --name hello-world
Created binary (application) package
At this point, you will have a very simple hello world project:
.
├── Cargo.toml
└── src
└── main.rs
This is the most basic Hello World application package, which simply prints out the string “Hello, world! “To run this program, you can use Cargo to build and execute the binary:
> cargo run
Compiling hello-world v0.1.0 (/Users/nremp/Projects/existing-directory)
Finished dev [unoptimized + debuginfo] target(s) in 0.64s
Running `target/debug/hello-world`
Hello, world!
You’ve now officially run your first Rust program!
Cargo will also look for entry points in the /src/bin
directory. For example, you can add a file called /src/bin/hello-bin.rs
and execute this program like so:
cargo run --bin hello-bin
lib vs. bin
In the previous example, we created a new Rust project with the command cargo new hello-world
. This command created a new binary package which, when compiled, produces an executable file that is intended to be run. In fact, we could have passed the —-bin
flag to be more explicit:
cargo new hello-world --bin
But what if we want to create a library designed to be imported by other projects instead? We can pass the —-lib
flag to cargo new
to create a library package in this case.
> cargo new my-lib --lib
Created library `my-lib` package
In this case, Cargo produces a different directory structure:
.
├── Cargo.toml
└── src
└── lib.rs
Notice how the example that is produced includes a unit test. Since we can’t run this library directly, we can include a test alongside our code to see how it works. You can use the command cargo test
to run the test. Pro-tip: if you add any print statements to your tests, you won’t see the output unless you use the —-nocapture
flag! You can read more about Rust’s test framework here.
> cargo test -- --nocapture
Finished test [unoptimized + debuginfo] target(s) in 0.01s
Running unittests src/lib.rs (target/debug/deps/my_lib-899502c70b06808d)
running 1 test
test result: 4
test tests::it_works ... ok
test result: ok. 1 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Doc-tests my-lib
running 0 tests
test result: ok. 0 passed; 0 failed; 0 ignored; 0 measured; 0 filtered out; finished in 0.00s
Putting it all together
A typical pattern when writing a Rust application is to start with a single module and split your work into multiple files only once the file length becomes unwieldy or when you want to share logic between numerous entry points. Let’s look at a simple example. Say we have the following main.rs:
// A simple program that takes a list of numbers
// as an input and prints the sum of the numbers.
fn main() {
let integers = std::env::args()
.skip(1)
.map(|x| x.parse::<i32>().unwrap())
.collect::<Vec<i32>>();
let sum = sum(&integers);
println!("Sum of {integers:?} is {sum}");
// Do other stuff...
}
fn sum(integers: &[i32]) -> i32 {
integers.iter().fold(0, |sum, x| sum + x)
}
Running this program produces the following output:
> cargo run 1 2 3 4 5
Finished dev [unoptimized + debuginfo] target(s) in 0.00s
Running `target/debug/hello-world 1 2 3 4 5`
Sum of [1, 2, 3, 4, 5] is 15
This is clearly a contrived example. But if for some reason, you wanted to call sum
from another module, you wouldn’t yet be able to. To do this, sum
will need to be moved to lib.rs
. What we end up with is something like the following:
.
├── Cargo.toml
└── src
├── bin
│ └── sum.rs
└── lib.rs
We can use the use
keyword to import the sum
function. Make sure you use the pub
keyword in the library so that it can be imported from other modules!
Workspaces
If your project grows large enough, you may want to consider breaking the project down even further into more components in what’s called a workspace. A workspace is a pattern used by Cargo that allows you to isolate related code into local crates in your project directory. Each crate has its own Cargo.toml
file, so you can control the size of the binary produced by the compiler. Workspaces also help with logical separation when dealing with many files.
Compiling Your Program
When you execute cargo run
, you will notice a new target
directory shows up in your project. This is because Cargo implicitly compiles your project in debug mode before running the output. If you wanted to, you could run the result directly. In our previous example, we had a sum.rs
file. This is compiled into a sum
binary for your machine’s architecture:
> ./target/debug/sum 1 2 3 4 5
Sum of [1, 2, 3, 4, 5] is 15
The default dev
release profile compiles much faster but actually runs much more slowly. When you want to deploy your work into some production environment, you will want to use the release profile to make your program 10-100x faster.
> cargo build --release
Compiling hello-world v0.1.0 (/Users/nremp/Projects/hello-world)
Finished release [optimized] target(s) in 0.76s
> ./target/release/sum 1 2 3 4 5
Sum of [1, 2, 3, 4, 5] is 15
These release profiles are configurable, but dev
and release
are some sane built-in defaults.
That wraps up my quick primer on how to get started writing Rust code. I hope this guide is helpful. If you have any questions, feedback, or requests for what I should write about, please let me know on Twitter at @nbrempel. If you found this article helpful, please consider sharing it with someone you think would find it useful. The more people read this, the more motivated I will be to keep writing!
Discussion