Rust Project Tips

Getting started, getting work done

Preface

All one must do to create a new Rust project is invoke a single line CLI incantation.

cargo new myproject

But that's the tip of the iceberg, so to speak. From there, everything becomes a matter of planning, knowledge and tooling.

Project Configuration

In the tone of Money Python: "What has Cargo new ever done for us?". The simple answer is that it:

At this point we have what we need to begin working. But is it enough? What else is there? Rust projects can be configured by a variety of files, existing in one or more locations. Let's look at a few of them.

Cargo.toml

The Cargo Manifest(Cargo.toml) is where we configure most aspects of our project including the:

There are facilities for setting up build-profiles as well.

Build Profiles

By default Cargo has development and release profiles, as well as test and bench profiles. Often, these are all you need. However, it can make life far easier to configure additional profiles and to alter the existing ones.

The following information is adapted from the memorysafety/rav1d repository.

No changes to the development profile:

[profile.dev]

A new profile called opt-dev which inherits from the development profile:

[profile.opt-dev]
inherits = "dev"
opt-level = 1

This adds some optimization to the development built which is considered slow in the scheme of things.

The release profile is updated to place all of the artefacts into a single compliation unit and to perform fat link-time-optimization

[profile.release]
codegen-units = 1
lto = "fat"

This facilitates more aggressive optimizations across the whole program at the cost of slower compilation times.

The next new profile is release-with-debug:

[profile.release-with-debug]
inherits = "release"
debug = "line-tables-only"

Development builds contain rich debugging information, but release builds perform aggressive optimization and do not insert the debugging information. So how are we to debug and benchmark a binary that is under-annotated, so to speak? By inheriting the release profile and adding some of the debugging information back in, of course!

These tips enable tools such as Perf and Valgrind to work, which are useful for tracking down bottlenecks and other performance issues.

.cargo/config.toml

The .cargo/config.toml file contains information including:

and much more.

If you need to supply some of the aforementioned information to rustc via Cargo, this is the place to look.

Some helpful aliases are shown in the documentation but with the addition of the build profiles I outlined you may wish to add more aliases. Here are a few configuration I use:

[alias]
b = "build"
c = "check --all-targets"
fc = "fmt --check"
t = "test"
r = "run"
rr = "run --release"
br = "build --release"

b-musl = "build --target=x86_64-unknown-linux-musl"
c-musl = "check --all-targets --target=x86_64-unknown-linux-musl"
fc-musl = "fmt --check --target=x86_64-unknown-linux-musl"
t-musl = "test --target=x86_64-unknown-linux-musl"
r-musl = "run --target=x86_64-unknown-linux-musl"
rr-musl = "run --target=x86_64-unknown-linux-musl --release"
br-musl = "build --target=x86_64-unknown-linux-musl --release"

[term]
color = "always"

[doc]
browser = "firefox"

Note: I don't use musl very often for these reasons.

rust-toolchain.toml

The rust-toolchain.toml documentation explains what we could achieve with this file.

For our purposes we may wish to

[toolchain]
channel = "1.90.0"
targets = [
    "aarch64-unknown-linux-gnu",
    "riscv64gc-unknown-linux-gnu",
    "x86_64-unknown-linux-gnu",
]
components = ["rustc"]

Unfortunately we can't build for multiple targets at once, so we would have to do something like this:

for target in \
    "aarch64-unknown-linux-gnu" \
    "riscv64gc-unknown-linux-gnu" \
    "x86_64-unknown-linux-gnu"
do
    cargo build --release --target="${target}"
done

But it's better to write a shell-script to do this neatly.

I often use the rust-toolchain.toml to set up the toolchain I need when using Snapcraft to build Snap packages, this prevents the tooling from not knowing which version of tooling to use.

Repository Structure

I like to have a top-level directory called releases in which I place my versioned compiled binaries, their GPG signatures, and the compressed Cargo.lock file. That means creating the directory and including it in the repository structure. I don't necessarily include the assets in the git history.

mkdir releases
touch releases/.gitkeep
git add releases/.gitkeep
echo '/releases' >> .gitignore
git commit -m 'chore: add /releases'

Then I have a script to build the project and to copy the binary executable etc into the directory. It's something like this (untested):

#!/bin/bash

# Get the project version
if ! Version="$(sed -nE '/^version\s+=\s+"[0-9]+\.[0-9]+\.[0-9]+"\s*$/ s/^version\s+=\s+"([0-9]+\.[0-9]+\.[0-9]+)"\s*$/\1/ p' Cargo.toml)"
then
    echo 'Failed to get version' >&2
    exit 1
fi

# Get the project name
if ! Name="$(sed -nE '/^name\s+=\s+"[^"]+"\s*$/ s/^name\s+=\s+"([^"]+)"\s*$/\1/ p' Cargo.toml)"
then
    echo 'Failed to get name' >&2
    exit 1
fi

declare -r BinaryName="${Name}-${Version}"
declare -r LockfileName="${Name}-${Version}-Cargo.lock.xz"

rm -f "releases/${BinaryName}"
rm -f "releases/${BinaryName}.asc"
rm -f "releases/${LockfileName}"
cp "target/release/${Name}" "releases/${BinaryName}" || exit 1
xz -k --stdout Cargo.lock > "releases/${LockfileName}" || exit 1

gpg --output "releases/${BinaryName}.asc" --armour --detach-sign "releases/${BinaryName}"

exit

Cargo has an unstable feature (--artifact-dir) which should help if/ when stabalised.

Tooling

I am an enthusiast of tools. Let's run through a handful of tools that I like to use:

Quality

cargo check

The first tool I use is cargo check to analyze my work for syntax issues without undertaking the time and resource intensive taks of compiling the source-code.

cargo clippy

Next, cargo clippy does the same thing cargo check does, with the addition of pointing out better ways of accomplishing tasks. i.e. that's bad, instead do this. Particularly useful when learning Rust as it will diabuse learners of bad habbits pretty quickly.

cargo fmt

cargo fmt can check your source files to determine whether they adhere to the standard style-guide (using --check) or to rewrite the files using the standard style-guide. It's possible to configure the tool to use your sytlistic preferences however I like to stick with the defaults are they largely to my preference.

cargo-audit

I really like cargo-audit, a tool which syncs the RustSec Advisory Database and then scans your Cargo.lock to search for packages whose versions have been marked as vulnerable.

I use this to check my software so I can update dependencies and publish a revised version. Such an occurrence can be seen here.

I have taken to shipping the Cargo.lock file with my software so those who wish can check the software for vulnerabilities.

e.g.

cargo audit \
    --file /snap/cert-checker/current/usr/share/cert-checker/Cargo.lock

Performance

cargo-criterion

Criterion is a really nifty benchmarking tool.

I am a casual user but I intend to swat up on criterion in the lead up to the festive season.

perf

perf is a sampling profiler. It periodically looks at what the CPU is executing at a point in time. The results are stored to a file, which can then be viewed with perf or converted into another form with other tools.

Record the samples:

sudo perf record your-application

View the sample:

perf report

I recommend changing the filename of the sampledata, and storing it in a subdirectory to get it out of the way

mkdir profiling
sudo perf record --output 'profiling/perf.data' your-application
sudo chmod 644 'profiling/perf.data'
perf report --input 'profiling/perf.data'

Hotspot

hotspot is available in the Ubuntu archives, (sudo apt install hotspot). It can convert the output of perf into a flamegraph:

hotspot /profiling/perf.data

Other tools can create SVG flamegraphs which can be loaded in a web-browser. Firefox Profiler can load the perf sample data directly, though application-confinement may interfere with that.

flamegraph

flamegraph profiles your application and then creates a flamegraph which can be viewed and analyzed to search for bottlenecks.

Utilities

cargo-watch

If you're writing a server of some kind, you may benefit from having cargo-watch watch for changes to your source files and recompile then re-launch your application. I have used it a handful of times when developing a webserver. I should use it more often.

cargo-license

cargo-license builds a list of the licenses used in your software dependencies.

cargo-expand

cargo-expand expands macros. This is particularly useful when you want to understand how a #[derive()] line, or a macro! has augmented your source-files. It isn't without its restrictions, but it is certainly a tool worth having in your arsenal.

Planning

If you're writing a command line application, consider doing things in this order:

Roundup

We've looked at some tips that I think will help you when making your Rust projects, and administering them thereafter. Go forth, make new and improved software.