Rust Development Tools You Should Know
Rust and cargo already provide great tooling out of the box, much to the delight of developers coming from languages like C or C++.
But the ecosystem is also full of community projects that significantly improve the development cycle. I have seen many new Rust developers struggle to discover these tools step by step.
To get you up to speed quickly, here is a small guide to all the projects I find useful. Each tool has a short introduction and usage examples.
Most will already know about the basic tools like cargo fmt
, clipppy
,
cargo doc
or rust-analyzer
, so they only get a cursory mention at the end.
A small index for the impatient reader:
- Macros
- cargo-expand: expand macros to actual code
- Dependency Management
- cargo-edit: easily add, remove and upgrade dependencies
- cargo-outdated: find outdated dependencies
- Profiling
- cargo-flamegraph: flamegraph generation with cargo
- criterion: statistics driven benchmark framework
- Code Coverage
- cargo-llvm-cov: Cargo wrapper for using LLVM based code coverage
- Tarpaulin: Rust-native coverage tool (only for Linux and x86)
- grcov: Tooling for merging and analyzing coverage data by LLVM and gcc
- Static Code Analysis
- cargo-llvm-lines: improve compile times by identifying monomorphization code bloat with LLVM IR
- cargo-bloat: code size analysis
- semverver: find backwards incompatible code changes
- Dependency Auditing
- cargo-geiger: find
unsafe
usage in crates - cargo-deny: prevent usage of dependencies based on license, source or vulnerabilities
- cargo-audit: check dependencies for known vulnerabilities
- cargo-crev: distributed, cryptographically verifiable code review system
- cargo-geiger: find
- Release Management
- cargo-release: automation for publishing/releasing crates
- Appendix: Builtins and IDEs
Macros
cargo-expand | |
---|---|
Repo | dtolnay/cargo-expand |
Install | cargo install cargo-expand |
OS Packages |
cargo-expand
is yet another tool by the venerable David Tolnay, creator of
anyhow
, serde
, syn
, and quote
, among many others.
It allows expanding macros and proc macros to actual code and is very useful for finding out what’s actually going on behind all the macro magic and is almost indispensable for debugging your own proc macros.
The command allows expanding a whole crate, submodules, specific functions, tests or examples.
Usage:
# Install:
cargo install cargo-expand
# Expand the whole crate:
cargo expand
# Expand specific submodules or functions:
cargo expand ::submodule
cargo expand ::submodule::my_function
# Expand a test:
cargo expand --test my_test
Ever wondered what println!()
actually does?
Example Output
fn f() {
println!("hello {}", 22);
}
cargo expand
pub fn f() {
{
::std::io::_print(::core::fmt::Arguments::new_v1(
&["hello ", "\n"],
&match (&22,) {
(arg0,) => [::core::fmt::ArgumentV1::new(
arg0,
::core::fmt::Display::fmt,
)],
},
));
};
}
Note: [rust-analyzer] can now also expand macros, which is much more convenient to use, since you can see the result right in your IDE.
But there are still a lot of situations where cargo-expand
is useful.
Dependency Management
cargo-edit | |
---|---|
Repo | killercup/cargo-edit |
Install | cargo install cargo-edit |
OS Packages |
cargo-edit is easily my most frequently used tool.
It provides the cargo add/rm/upgrade
subcommands that vastly simplify adding
and upgrading dependencies.
cargo add
Here are the steps you normally take to add a dependency:
- Switch to browser
- Go to crates.io to find the latest version number
- Copy the version specification
- Open
Cargo.toml
- Find the
[dependencies]
section - Paste
- Repeat for the next dependency
If you already know the name(s), this now becomes: cargo add CRATE1 CRATE2 ...
.
The command will look up the latest version in the local cargo registry,
normalize between dashes and underscores in the crate name (in case you typed it wrong),
and append the dependency to Cargo.toml
.
As someone working with a lot of different Rust crates, I would probably go crazy without it.
There are also multiple flags for common configurations.
Some examples:
# Add multiple dependencies:
cargo add tracing tracing-subscriber
# Add a devleopment dependency:
cargo add --dev criterion
# Add a build dependency
cargo add --build cc
# Specify features:
cargo add uuid --features serde v4
# Disable default features
# (Adds `default-features = false`)
cargo add --no-default-features tokio
# Use the latest pre-release (alpha/beta) version if available
cargo add --allow-pre-release clap
cargo rm
cargo rm CRATE1 ...
allows removing dependencies.
# Delete dependencies
cargo rm tokio futures reqwest
# Delete dev dependency
cargo rm --dev criterion
cargo upgrade
Cargo has a built in command cargo-update
, which can be a bit misleading.
This will update the Cargo.lock
lock file with the newest available version
that fits the version constraints specified in Cargo.toml
. If your version
constraint looks like serde = "1.0.0"
, it will update serde to the newest
1.0.x release.
But it will not change the versions in Cargo.toml
, or warn you about available
major version upgrades.
cargo upgrade
, in contrast, will do exactly that.
After running it, always manually check the applied changes, and make sure that
you actually want to do the upgrade. Don’t forget that incompatible version
upgrades in a library also force the library to do a major version bump. (0.1.x
=> 0.2.0
or 1.x.x
=> 2.0.0
).
cargo upgrade
# Upgrade the whole workspace
cargo upgrade --workspace
cargo-outdated | |
---|---|
Repo | kbknapp/cargo-outdated |
Install | cargo install cargo-outdated |
OS Packages |
While cargo upgrade allows automatically upgrading all dependencies, you might first want to check what changes would be made.
cargo outdated
will print a table comparing the versions in Cargo.lock
with
the newest available version in the registry.
NOTE: you will probably also want to use the -R
/ --root-deps-only
flag.
Without it the list will contain all transitive/nested crates, which can be
very noisy and confusing.
Here is an example output:
cargo outdated --root-deps-only
Name Project Compat Latest Kind Platform
---- ------- ------ ------ ---- --------
anyhow 1.0.40 1.0.44 1.0.44 Normal ---
bincode 1.3.2 1.3.3 1.3.3 Normal ---
bytes 1.0.1 1.1.0 1.1.0 Normal ---
crossbeam 0.8.0 0.8.1 0.8.1 Normal ---
Profiling
cargo-flamegraph | |
---|---|
Repo | flamegraph-rs/flamegraph |
Install | cargo install flamegraph NOTE: also requires some additional setup - consult the README |
OS Packages |
This project brings effortless flamegraph generation via cargo. It supports binaries, tests and examples.
About Flamecharts
If you don’t know about flamegraphs yet, then: go learn about them!
Flame graphs (also called flame charts) are a visualization of runtime characteristics, like the time spent running each function. They are incredibly useful for identifying bottlenecks and finding potential optimisations.
They were pioneered by Netflix engineer Brendan Gregg, and have even found their way into the browser devtools in Firefox and Chrome.
You can find out more in this Youtube presentation
or in the flamegraph-rs
README.
Generating Flamecharts
cargo-flamegraph
might need some additional setup steps to run, like
installing perf
tools on Linux, granting relevant permissions or enabling debug
info in release builds. Consult the
README for the details.
But in general, generating graphs is effortless:
# Test the main binary
cargo flamegraph
# >> More likely you will want to use a benchmark, example, or test:
# Run `examples/stress.rs`
cargo flamegraph --example stress
# Run `tests/stress.rs`
cargo flamegraph --test stress
These commands will generate a flamegraph.svg
file in you current directory.
The SVG is interactive and allows drilling down into particular segments.
Click the image below to open the interactive version.
criterion | |
---|---|
Repo | bheisler/criterion.rs |
criterion
is a statistics driven benchmark library. It will try to repeatedly
run benchmarks until the timings are statistically significant.
It has lot’s of features, including:
- defining and grouping many different benchmarks
- directly comparing two different implementations
- comparing results to the previous run (great for iterating on a problem!)
- generating plots and graphs (regressions, distributions, …)
- …
The results can be printed in the terminal, but criterion also has HTML reports that can also be generated in a CI job.
An introduction is out of scope here, but the project has good documentation.
Check out the quickstart and the user guide.
Alternative: There is also another project by the same author: iai. It uses cachgrind and can be a lot faster to run, but isn’t actively developed.
Side note: cargo also has a built-in simple benchmark library, but it has been restricted to nightly Rust for years and isn’t nearly as feature rich. criterion should be preferred in most situations.
Code Coverage
Code coverage is an important part of testing, and often used in CI workflows.
Sadly all the coverage solutions currently available come with trade offs and limitations. I have listed the three most useful solutions here.
cargo-llvm-cov | |
---|---|
Repo | taiki-e/cargo-llvm-cov |
Install | cargo install cargo-llvm-cov |
OS Packages |
cargo-llvm-cov
utilizes the fairly new
built-in code coverage
support in rustc
.
It automates supplying the -Z instrument-coverage
flag and generating reports.
Pro: very easy to install and use, uses builtin coverage support.
Cons: only works on nightly
Rust, has some issues with doctests, and only supports line based coverage, not branch coverage.
lcov
reports are also supported, which can be uploaded to sites like
codecov.io.
Usage:
# Install:
rustup component add llvm-tools-preview --toolchain nightly
cargo install cargo-llvm-cov
# Run tests and print results to stdout:
cargo llvm-cov
# Run tests and generate HTML report:
cargo llvm-cov --html
# Run tests, generate HTML report and open in browser:
cargo llvm-cov --open
# Convert the results of the previous run into a different format (eg lcov):
cargo llvm-cov --no-run --lcov
tarpaulin | |
---|---|
Repo | xd009642/tarpaulin |
Install | cargo install cargo-tarpaulin |
OS Packages |
tarpaulin
is a code coverage tool written in Rust and uses custom instrumentation.
Pros:
- easy to use
- exclude code with
#[cfg(...)]
attributes - reports change in coverage between runs
- text, html and lcov reports
- built in codecov/coveralls upload integration
Cons:
- ONLY works on Linux and x86 (!)
- only supports line coverage
Usage:
# Install:
cargo install cargo-tarpaulin
# Generate coverage:
cargo tarpaulin
Check the docs for ignoring code with attributes or for automatic coverall/codecov uploads.
grcov | |
---|---|
Repo | mozilla/grcov |
Install | cargo install grcov |
OS Packages |
grcov
is a project by Mozilla.
The main distinguishing feature is that is built around combining and aggregating multiple reports, which makes it easier to aggregate reports across multiple architectures, different cargo projects or different languages.
Sadly it doesn’t have a convenient cargo wrapper (yet), which makes it quite a bit complicated to use.
Consult the usage docs to see some examples.
Static Code Analysis
cargo-llvm-lines | |
---|---|
Repo | dtolnay/cargo-llvm-lines |
Install | cargo install cargo-llvm-lines |
OS Packages |
cargo-llvm-lines
is another tool by the venerable David Tolnay, creator of
anyhow
, serde
, syn
, and quote
, among many others.
It has helped me to significantly speed up compile times in a few projects, so it should be in the tool belt for developers working on larger projects.
A bit of background first: Rust relies heavily on generics. At compile time each generic function application has to be monomorphized, which means generating code for the types that are actually used. This is made worse by the fact that in Rust the compilation unit is a crate, so code generation happens once for every crate.
This can result in excessive amounts of code, which means a lot of extra work for the compiler and also increased binary size.
A common workaround is to find problematic generics, and replace them with either
- a non-generic function that takes a trait object instead (like
&dyn T
,Box<dyn T>
, etc) - Using a very short generic wrapper that delegates to a non-generic functions
Finding such opportunities is where cargo-llvm-lines
comes into the picture.
It counts the lines of LLVM IR
(the intermediate representation for the LLVM toolchain which is generated by rustc
)
generated for each generic function and shows a sorted list that highlights the
most costly occurrences.
The output will look something like this:
Lines Copies Function name
----- ------ -------------
163525 (100%) 4309 (100%) (TOTAL)
8613 (5.3%) 168 (3.9%) core::result::Result<T,E>::map
7314 (4.5%) 46 (1.1%) <serde_json::value::de::MapDeserializer as serde::de::MapAccess>::next_key_seed
6920 (4.2%) 57 (1.3%) serde_json::value::de::visit_array
5885 (3.6%) 48 (1.1%) serde_json::value::de::<impl serde::de::Deserializer for serde_json::value::Value>::deserialize_struct
5831 (3.6%) 48 (1.1%) serde_json::value::de::visit_object
...
...
cargo-bloat | |
---|---|
Repo | RazrFalcon/cargo-bloat |
Install | cargo install cargo-bloat |
OS Packages |
cargo-bloat
is very similar to cargo-llvm-lines introduced
above, so it is useful for the same reasons.
Instead of working with the LLVM IR, cargo-bloat
analyses the final binary
output. That means the results will be accurate and not affected by LLVM
optimization passes, but it is also slower to run.
Hence it is more useful for reducing actual code size, but less useful for improving compile times.
It also has some extra features like summarizing by dependency and filtering with a regex.
# Get the 20 largest functions:
cargo bloat --release -n 20
# Show the biggest dependencies:
cargo bloat --release --crates
Example output:
File .text Size Crate
2.7% 6.2% 970.3KiB std
1.0% 2.3% 359.0KiB serde_json
...
semverver | |
---|---|
Repo | rust-lang/rust-semverver |
Install | Installation requires a specific nightly version. Consult the Installation docs. |
semverver
is a little-known tool, but a very useful one.
It identifies API changes between two versions of a crate and can highlight changes that break compatibility according to the official Rust API Guidelines.
The Rust ecosystem follows the semantic versioning
specification, with the addition that patch releases in 0.x
versions are
usually also considered non-breaking. So even a 0.3.y
release should not break any other 0.3.x
users.
Accidentally introducing a breaking change can be quite easy, though, especially if a release contains a lot of changes.
Manually auditing all commits can be quite a chore, so semver
can be a big help,
especially if it’s run in a CI task.
Installation requires a specific nightly version, so follow the official docs.
Once installed, running cargo semver
(with the specific nightly!) will
compare the current crate against the newest version published on crates.io
.
You can also compare to a specific version by specifying a path, for example to an older git checkout.
# Compare against crates.io version:
cargo semver --explain
# Compare against local checkout:
cargo semver --explain --stable-path ~/my-crate-old
The output isn’t perfect, and sometimes mis-categorizes, but most changes are detected correctly.
Example Output
```
version bump: 0.1.0 -> (breaking) -> 0.1.1
error: breaking changes in `S`
--> /home/theduke/tmp/semver2/src/lib.rs:3:1
|
3 | / pub struct S {
4 | | pub a: String,
5 | | b: bool,
6 | | pub c: String,
7 | | }
| |_^
|
warning: public field added to struct with private fields (breaking)
--> /home/theduke/tmp/semver2/src/lib.rs:6:5
|
6 | pub c: String,
| ^^^^^^^^^^^^^
error: breaking changes in `E`
--> /home/theduke/tmp/semver2/src/lib.rs:11:1
|
11 | / pub enum E {
12 | | A,
13 | | }
| |_^
|
warning: enum variant removed (breaking)
--> /home/theduke/tmp/semver1/src/lib.rs:10:5
|
10 | B,
| ^
warning: technically breaking changes in `T`
--> /home/theduke/tmp/semver2/src/lib.rs:15:1
|
15 | / pub trait T {
16 | | fn t1() -> char;
17 | |
18 | | fn with_default() {
19 | |
20 | | }
21 | | }
| |_^
|
note: added defaulted item to trait (technically breaking)
--> /home/theduke/tmp/semver2/src/lib.rs:18:5
|
18 | fn with_default() {
| ^^^^^^^^^^^^^^^^^
error: breaking changes in `t1`
--> /home/theduke/tmp/semver2/src/lib.rs:16:5
|
16 | fn t1() -> char;
| ^^^^^^^^^^^^^^^^
|
= warning: type error: expected `bool`, found `char` (breaking)
warning: path changes to `S2`
--> /home/theduke/tmp/semver2/src/lib.rs:9:1
|
9 | pub struct S2;
| ^^^^^^^^^^^^^^
|
= note: added definition (technically breaking)
error: aborting due to 3 previous errors; 2 warnings emitted
error: rustc-semverver errored
```
Dependency Auditing
cargo-geiger | |
---|---|
Repo | rust-secure-code/cargo-geiger |
Install | cargo install cargo-geiger |
OS Packages |
cargo-geiger
analyses the dependency tree for use of unsafe
and prints out
a color-coded tree view showing the amount of unsafe code.
It is aptly named after the Geiger counter, a device to measure ionizing radiation.
unsafe
is an unavoidable part of every Rust project. It is required to do
anything useful, since the OS APIs are all unsafe. std
and other crates
implementing advanced data structures also almost always have at least some
unsafe
code in them.
But in many cases, unsafe
can be avoided.
cargo-geiger
is useful for auditing the dependency tree and identifying crates
that might require special attention before including them in a project.
Simply run cargo geiger
in your crate.
cargo-deny | |
---|---|
Repo | EmbarkStudios/cargo-deny |
Install | cargo install cargo-deny |
OS Packages |
cargo-deny
validates your dependencies against configurable policies.
It is most useful as a CI check or as a manual safeguard before publishing.
Features:
- Audit the licenses used by dependencies, and shows errors for licenses you want to reject
- Check dependencies for vulnerabilities that are present in RustSec Advisory Database.
- Limit allowed dependency sources (cargo registries, specific Git repositories, Github organizations, …)
- Manually ban specific dependencies
It also provides a fix
command that automatically upgrades crates with
vulnerabilities, and a list
command that lists the licenses of all dependencies.
Configuration options are described in the documentation.
There is also a dedicated Github action.
Usage:
# Install
cargo install cargo-deny
cd my/project
# Generate the default configuration.
cargo deny init
# Check all the configured policies
cargo deny check
# Auto-upgrade vulnerable dependencies
cargo deny fix
# List dependency licenses
cargo deny list
cargo-audit | |
---|---|
Repo | RustSec/rustsec |
Install | cargo install cargo-audit |
OS Packages |
cargo-audit
provides a subset of the functionality of cargo-deny.
It checks all dependencies present in Cargo.lock
for known
vulnerabilities that are present in RustSec Advisory Database,
a community maintained collection of security issues in published crates.
To execute, just run cargo audit
.
You can easily run it as an extra check in your CI. For Github there is also a dedicated Github action.
Note: you should probably stick to just using cargo-deny, since it provides the same functionality (and more).
cargo-crev | |
---|---|
Repo | crev-dev/cargo-crev |
Install | cargo install cargo-crev |
OS Packages |
With all the supply chain attacks we have seen in recent years, auditing our dependencies is becoming ever more important. Especially in a business context.
Actually doing manual audits a huge amount of work though. Generally only large companies can afford to do this properly, and even then most of them still end up pulling in random packages without proper screening.
I argue that Rust is somewhat more vulnerable here than other languages. The relatively small standard library and still evolving ecosystem often leads to hundreds of dependencies and very regular version bumps.
cargo-crev
is a fascinating system for distributed, shared and
cryptographically verifiable auditing of dependencies that aims to help with
this problem.
Users can publish proofs, which are manual code reviews of a crate. Proofs are signed with a cryptographic identity and can be shared with others through Git repositories.
This allows to share the work of auditing and to build up a chain of trust. You can decide if you want to trust prominent community members, or maybe keep all proofs inside your company.
crev
is also language agnostic in theory, though the only actual implementation
is for Rust and cargo.
cargo-crev
also includes checks done by other tools like
cargo-geiger and cargo-audit.
Usage
Using cargo-crev
entails creating your own identity, potentially trusting
other users and importing their proofs, creating and publishing your own proofs,
and also validating your crates.
The Getting Started guide has you covered.
Release Management
cargo-release | |
---|---|
Repo | crate-ci/cargo-release |
Install | cargo install cargo-release |
OS Packages |
cargo-release
simplifies and automates the workflow for publishing crates to
the crates.io registry.
If you maintain published crates, you will really appreciate workflow improvement.
It handles many steps automatically, like:
- ensuring a clean repository
- bumping the version in
Cargo.toml
, - updating a changelog
- replacing version strings in documentation and other files (must be configured)
- creating and pushing a Git tag
- releasing multiple crates in a cargo workspace
Using it is as simple as running cargo release patch/minor/major
(This is safe to try out, the --execute
flag is required to actually publish).
The many configuration options are described in the documentation.
Alternative: There is also a much newer alternative, cargo-smart-release.
It aims to solve a few pain points of cargo-release
, especially related to multi-crate
workspaces. I haven’t personally used it, but it is worth a look.
Appendix: Builtins and IDEs
Most users will already be familiar with the built in tooling and the IDE integrations.
For completeness sake, here is a short summary:
Built-in Tooling
- rustfmt
the canonical Rust code formatter
Usually used viacargo fmt
- clippy
Linter that performs additional checks and can find quite a few anti-patterns
Used viacargo clippy
IDEs
- rust-analyzer LSP (language server protocol) implementation for Rust with plugins for many editors and IDEs.
- Intellij Rust Official Rust plugin for Intellij IDEs (CLion, Intellij Ultimate, …)