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
  • Dependency Management
  • Profiling
  • 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
  • Release Management
  • Appendix: Builtins and IDEs

Macros


cargo-expand

Repodtolnay/cargo-expand
Installcargo install cargo-expand
OS PackagesPackaging status

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

Repokillercup/cargo-edit
Installcargo install cargo-edit
OS PackagesPackaging status

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

Repokbknapp/cargo-outdated
Installcargo install cargo-outdated
OS PackagesPackaging status

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

Repoflamegraph-rs/flamegraph
Installcargo install flamegraph
NOTE: also requires some additional setup - consult the README
OS PackagesPackaging status

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.

Example Flamegraph


criterion

Repobheisler/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

Repotaiki-e/cargo-llvm-cov
Installcargo install cargo-llvm-cov
OS PackagesPackaging status

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

Repoxd009642/tarpaulin
Installcargo install cargo-tarpaulin
OS PackagesPackaging status

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

Repomozilla/grcov
Installcargo install grcov
OS PackagesPackaging status

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

Repodtolnay/cargo-llvm-lines
Installcargo install cargo-llvm-lines
OS PackagesPackaging status

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

RepoRazrFalcon/cargo-bloat
Installcargo install cargo-bloat
OS PackagesPackaging status

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

Reporust-lang/rust-semverver
InstallInstallation 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

Reporust-secure-code/cargo-geiger
Installcargo install cargo-geiger
OS PackagesPackaging status

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.

Screenshot showing cargo geiger terminal output


cargo-deny

RepoEmbarkStudios/cargo-deny
Installcargo install cargo-deny
OS PackagesPackaging status

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

RepoRustSec/rustsec
Installcargo install cargo-audit
OS PackagesPackaging status

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.

Example output

Note: you should probably stick to just using cargo-deny, since it provides the same functionality (and more).


cargo-crev

Repocrev-dev/cargo-crev
Installcargo install cargo-crev
OS PackagesPackaging status

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

Repocrate-ci/cargo-release
Installcargo install cargo-release
OS PackagesPackaging status

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 via cargo fmt
  • clippy
    Linter that performs additional checks and can find quite a few anti-patterns
    Used via cargo 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, …)