Rust Pro Tips (collection)

Level up your Rust skills.

 

This is a collection of Rust “pro tips” that I’ve collected, most of which have been posted on Twitter. I’ll keep updating this post as I write more. Tips are ordered in reverse chronological order, with the most recent ones at the top.

35. Force compilation failure

Tweet Toot

The compile_error!(...) macro forces compilation failure. This can be useful to indicate unsupported feature flag combinations or invalid macro arguments.

compile_error!("Boo!");

fn main() {
    println!("Hello, world!");
}

Compiler output:

   Compiling playground v0.0.1 (/playground)
error: Boo!
 --> src/main.rs:1:1
  |
1 | compile_error!("Boo!");
  | ^^^^^^^^^^^^^^^^^^^^^^

error: could not compile `playground` (bin "playground") due to 1 previous error

Playground
Docs


Bonus tip #1: Emit compiler warnings using build.rs:

fn main() {
    #[cfg(feature = "funky-time")]
    println!("cargo::warning=Funky mode is enabled!");
}

Bonus tip #2: Emit structured diagnostics from proc macros using the nightly Diagnostic API. (tracking issue #54140)

34. Enable optional dependency features with a feature

Tweet Toot

Use the ? syntax in Cargo.toml to activate features on optional dependencies only when those dependencies are enabled.

[dependencies]
backend-a = { version = "1", optional = true }
backend-b = { version = "1", optional = true }

[features]
default = ["backend-a"]
unstable = ["backend-a?/unstable", "backend-b?/unstable"]
# Enabling the "unstable" feature won't implicitly enable either backend.

Docs

33. Use tuple struct initializers as function pointers

Tweet Toot

Tuple struct initializers can be cast to function pointers. This can help to avoid creating unnecessary lambda functions, e.g. when calling Iterator::map.

#[derive(Debug, PartialEq, Eq)]
struct Point(i32, i32);

fn zeroes<T>(f: fn(i32, i32) -> T) -> T {
    f(0, 0)
}

assert_eq!(zeroes(Point), Point(0, 0));

Playground
Docs

32. Absolute import paths

Tweet Toot

Use a leading double colon (::path) to indicate a path relative to the root of the compilation unit. This can help to avoid name collisions when writing macros.

#[cfg(ignore)] // Remove to cause a name collision.
pub mod std {
    pub mod cmp {
        pub enum Ordering {
            Equal,
        }

        impl Ordering {
            pub fn is_eq(self) -> bool {
                panic!("Bamboozled!");
            }
        }
    }
}

macro_rules! create_function {
    () => {
        // Try replacing the next line with the one below it.
        fn print_is_equal(o: std::cmp::Ordering) {
        // fn print_is_equal(o: ::std::cmp::Ordering) {
            println!("is equal? {}", o.is_eq());
        }
    }
}

create_function!();
print_is_equal(std::cmp::Ordering::Equal);

Playground
Docs

31. Use indirection in enums to save memory

Tweet Toot

All variants of an enum are the same size. This can become a problem when variants have drastically different memory requirements. Use indirection (&-reference, Box, etc.) to resolve.

enum WithoutIndirection {
    Unit,
    Kilobyte([u8; 1024]),
}

enum WithIndirection<'a> {
    Unit,
    Kilobyte(&'a [u8; 1024]),
}

println!("{}", std::mem::size_of_val(&WithoutIndirection::Unit));
// => 1025

println!("{}", std::mem::size_of_val(&WithIndirection::Unit));
// => 8

Playground
Docs

30. Create a slice from a reference without copying

Tweet Toot

If you have a reference to some data and you want to pass it to a function that takes a slice, you can use std::array::from_ref to cheaply create a slice without copying.

struct Thing;

fn takes_slice(_: &[Thing]) {
    // ...
}

fn my_function(arg: &Thing) {
    takes_slice(std::array::from_ref(arg));
}

fn main() {
    my_function(&Thing);
}

Playground

29. Include README in documentation

Tweet Toot

Insert the README into a crate’s documentation by putting #![doc = include_str!("../README.md")] in lib.rs. This can help avoid duplicating information between the README and the documentation, and will also test README examples as doctests.

// lib.rs
#![doc = include_str!("../README.md")]

Docs
Documentation from a crate that does this

28. Workspace dependencies

Tweet Toot

Easily specify a uniform dependency version for all crates in a workspace with the [workspace.dependencies] table in Cargo.toml.

# Cargo.toml

[workspace]
members = ["my_crate"]

[workspace.dependencies]
serde = "1.0.183"
# my_crate/Cargo.toml

[dependencies]
serde.workspace = true # -> 1.0.183

Docs

27. Testing for compilation failure

Tweet

Create tests intended to fail compilation with the compile_fail attribute on documentation tests.

/// ```compile_fail
/// my_function("hello");
/// ```
pub fn my_function(value: u8) {}

The compilation error is error[E0308]: mismatched types, which you can check for specifically:

/// ```compile_fail,E0308
/// my_function("hello");
/// ```
pub fn my_function(value: u8) {}

Docs

26. Sealed traits

Tweet

If a public trait requires the implementation of a private trait, the public trait is “sealed” and can only be implemented within the crate that defines it.

// my_crate/src/lib.rs

mod private {
    pub trait Sealed {}

    impl Sealed for u8 {}
}

pub trait PublicTrait: private::Sealed {}

impl PublicTrait for u8 {}
// another_crate/src/lib.rs

use my_crate::PublicTrait;

// error: requires private::Sealed implementation
impl PublicTrait for String {}

// error: my_crate::private::Sealed is private
impl my_crate::private::Sealed for String {}

Rust API Guidelines

25. Static type size assertion

Tweet

Use std::mem::transmute::<S, D> to assert that two types have the same size at compile time.

use std::mem::transmute;

struct TwoBytes(u8, u8);

let _ = transmute::<TwoBytes, u16>; // ok
let _ = transmute::<TwoBytes, u32>; // error!

The compiler will complain if the types are not the same size:

error[E0512]: cannot transmute between types of different sizes, or dependently-sized types

Warning: Memory layout, alignment, etc. is often not guaranteed, so be careful!

Playground
Docs

24. Conditional compilation

Tweet

Use the cfg and cfg_attr attributes to compile different code based on the build environment. This is useful for feature-gating, platform-specific code, etc.

// Implements Serialize if the crate is built with the "serde" feature enabled
#[cfg_attr(feature = "serde", derive(serde::Serialize))]
struct Point2(f64, f64);

fn main() {
    #[cfg(target_os = "linux")]
    let person = "Linus Torvalds";
    #[cfg(target_os = "windows")]
    let person = "Bill Gates";
    #[cfg(target_os = "macos")]
    let person = "Tim Apple";

    // Approval-seeking!
    println!("Hi there, {person}!");
}

Docs
Playground

23. Declaratively create HashMaps from iterables

Tweet

HashMaps can be built from iterators of key-value tuples:

use std::collections::HashMap;

let map: HashMap<&str, i32> = [
    ("a", 1),
    ("b", 2),
    ("c", 3),
    ("d", 4),
]
.into();

Playground
Docs

22. impl vs. dyn in assembler

Tweet

Use impl for generic monomorphization, dyn for dynamic dispatch.

fn with_impl<'a>(v: &'a impl AsRef<[u8]>) -> &'a str {
    std::str::from_utf8(v.as_ref()).unwrap()
}

fn with_dyn<'a>(v: &'a dyn AsRef<[u8]>) -> &'a str {
    std::str::from_utf8(v.as_ref()).unwrap()
}

pub fn main() {
    let array = [72, 101, 108, 108, 111];
    let vector = vec![72, 101, 108, 108, 111];
    assert_eq!("Hello", with_impl(&array));
    assert_eq!("Hello", with_dyn(&array));
    assert_eq!("Hello", with_impl(&vector));
    assert_eq!("Hello", with_dyn(&vector));
}

See the effect in the generated assembly. with_impl is generated once for each parameterization. with_dyn is generated once, but a vtable is required at runtime.

; with_impl(&[u8; 5]) implementation (monomorphized)
_ZN7example9with_impl17h38cd8c9aec305c25E:
        sub     rsp, 40
        ; constant lookup
        mov     rax, qword ptr [rip + _ZN4core5array92_$LT$impl$u20$core..convert..AsRef$LT$$u5b$T$u5d$$GT$$u20$for$u20$$u5b$T$u3b$$u20$N$u5d$$GT$6as_ref17h86d8b288e84fcf8aE@GOTPCREL]
        call    rax
        mov     rsi, rax
        ; [[snip]]

; with_impl(&Vec<u8>) implementation (monomorphized)
_ZN7example9with_impl17hd50582b2776df596E:
        sub     rsp, 40
        ; constant lookup
        mov     rax, qword ptr [rip + _ZN88_$LT$alloc..vec..Vec$LT$T$C$A$GT$$u20$as$u20$core..convert..AsRef$LT$$u5b$T$u5d$$GT$$GT$6as_ref17h56f6b2151f0bc49cE@GOTPCREL]
        call    rax
        mov     rsi, rax
        ; [[snip]]

; with_dyn(&dyn AsRef<[u8]>) fat pointer implementation
_ZN7example8with_dyn17h88123f4b1a0b56e4E:
        sub     rsp, 40
        ; get vtable entry
        mov     rax, qword ptr [rsi + 24]
        call    rax
        mov     rsi, rax
        ; [[snip]]

When invoking with_dyn, the computer must construct the vtable at runtime.

        ; [[snip]]
        ; with_dyn(&Vec<u8>) invocation
        ; load the vtable
        lea     rsi, [rip + .L__unnamed_14]
        lea     rdi, [rsp + 176]
        call    _ZN7example8with_dyn17h88123f4b1a0b56e4E
        ; [[snip]]

.L__unnamed_14:
        .quad   _ZN4core3ptr46drop_in_place$LT$alloc..vec..Vec$LT$u8$GT$$GT$17hec70dc68d599b27eE
        .asciz  "\030\000\000\000\000\000\000\000\b\000\000\000\000\000\000"
        .quad   _ZN88_$LT$alloc..vec..Vec$LT$T$C$A$GT$$u20$as$u20$core..convert..AsRef$LT$$u5b$T$u5d$$GT$$GT$6as_ref17h56f6b2151f0bc49cE

Compare this to the invocation of a monomorphized function:

        ; [[snip]]
        ; with_impl(&Vec<u8>) invocation
        lea     rdi, [rsp + 176]
        call    _ZN7example9with_impl17hd50582b2776df596E
        ; [[snip]]

No vtable required!

dyn docs
impl docs

21. Closure traits

Tweet

Rule of thumb: the more a closure does with captured variables, the fewer traits it implements.

Only reads? Fn + FnMut + FnOnce.
Mutates? FnMut + FnOnce.
Moves? FnOnce.

No captures at all? Function pointer coercion, too!

fn impl_fn_once(_: &impl FnOnce() -> ()) {}
fn impl_fn_mut(_: &impl FnMut() -> ()) {}
fn impl_fn(_: &impl Fn() -> ()) {}
fn fn_pointer(_: fn() -> ()) {}

#[derive(Debug)]
struct Var(i32);

{ // A
    let mut var = Var(0);
    let f = || drop(var);
    impl_fn_once(&f);
    impl_fn_mut(&f);
    impl_fn(&f);
    fn_pointer(f);
}

{ // B
    let f = || println!("{:?}", Var(0));
    impl_fn_once(&f);
    impl_fn_mut(&f);
    impl_fn(&f);
    fn_pointer(f);
}

{ // C
    let mut var = Var(0);
    let f = || println!("{:?}", var);
    impl_fn_once(&f);
    impl_fn_mut(&f);
    impl_fn(&f);
    fn_pointer(f);
}

{ // D
    let mut var = Var(0);
    let f = || var.0 += 1;
    impl_fn_once(&f);
    impl_fn_mut(&f);
    impl_fn(&f);
    fn_pointer(f);
}

Rust Book
Rust Reference
Playground

20. Write better tests with #[should_panic]

Tweet

Require tests to panic with #[should_panic]. This is useful for testing unhappy paths. Optionally include a substring to match against the panic message.

#[test]
#[should_panic = "attempt to divide by zero"]
fn div_zero() {
    let val = 1i32.div(0);
}

Docs

19. Use #[non_exhaustive] to prevent breaking changes

Use the #[non_exhaustive] attribute to prevent breaking changes when adding new fields to a struct or enum variant.

#[non_exhaustive]
pub enum Error {
    Io(IoError),
    ParseInt(ParseIntError),
    ParseFloat(ParseFloatError),
}

This doesn’t make much a difference in the crate where the type is defined, but it’s useful for downstream crates (those that depend on your crate), since it prevents breaking changes when new variants are added.

So, this is fine, but only within the crate that defines the type:

match e {
    Error::Io(_) => println!("IO error"),
    Error::ParseInt(_) => println!("Parse int error"),
    Error::ParseFloat(_) => println!("Parse float error"),
}

However, a crate that depends on your crate must include a wildcard pattern to handle new variants:

match e {
    Error::Io(_) => println!("IO error"),
    Error::ParseInt(_) => println!("Parse int error"),
    Error::ParseFloat(_) => println!("Parse float error"),
    _ => println!("Other error"),
}

Docs

18. Union types

Tweet

One of Rust’s lesser-known composite types is the union. While they might look like structs at first, each field is actually the same piece of memory, allowing you to reinterpret bytes as a different type. Of course, this requires unsafe code.

union Onion {
    uint: u32,
    tuple: (u16, u16),
    list: [u8; 4],
}

let mut o = Onion { uint: 0xAD0000 };

unsafe {
    o.tuple.0 = 0xBEEF;
    o.list[3] = 0xDE;
}

assert_eq!(unsafe { o.uint }, 0xDEADBEEF);

(Extra pro tip: you can do pattern matching on unions too!)

Playground
Docs

17. Struct update operator ..

Tweet

Use the struct update operator .. to easily copy a struct with a few minor modifications. This can be useful when a struct implements Default.

#[derive(Default)]
struct Post {
    title: String,
    body: String,
    image_url: Option<String>,
    view_count: u32,
    tags: Vec<String>,
}

let p = Post {
    title: "How to make $1M posting coding tips on Twitter".into(),
    body: "1. Post coding tips on Twitter.\n2. ???\n3. Profit!".into(),
    ..Default::default()
};

assert_eq!(p.image_url, None);
assert_eq!(p.view_count, 0);
assert_eq!(p.tags, Vec::<String>::new());

Playground

16. The @ operator

Tweet

Use the @ operator to bind an identifier to a value that matches a pattern.

struct User {
    age: u8,
    name: String,
}

let u = User { age: 25, name: "John".into() };

if let User { age: a @ ..=35, .. } = u {
    println!("Become POTUS in {} years", 35 - a);
}

Playground
Documentation
Rust by Example

15. Create your own iterators

Tweet

Rust’s Iterator trait is super useful! Here’s how to implement it:

We’ll start with a data structure we want to iterate over:

struct Pair<T> {
    a: T,
    b: T,
}

And a function that returns the iterator:

impl<T> Pair<T> {
    pub fn iter(&self) -> PairIter<T> {
        // ...
    }
}

Here’s our iterator struct. It keeps track of:

  1. What collection we’re iterating over.
  2. The current location within the collection.
struct PairIter<'a, T> {
    pair: &'a Pair<T>,
    next: u8,
}

Let’s implement Iterator. Note that the type of the items emitted by the iterator is defined by an associated type.

impl<'a, T> Iterator for PairIter<'a, T> {
    type Item = &'a T;

    fn next(&mut self) -> Option<Self::Item> {
        // ...
    }
}

The next() function simply needs to update the iterator and return the current item.

match self.next {
    0 => {
        self.next += 1;
        Some(&self.pair.a)
    }
    1 => {
        self.next += 1;
        Some(&self.pair.b)
    }
    _ => None,
}

And that’s it! Now you can easily implement Iterator on your own types.

Playground
Docs

14. Use longer names for lifetimes and generic parameters

Tweet

Lifetimes and generic parameters don’t have to be just one character long. When you’re deep in the weeds, working your Rust magic, keep your code comprehensible.

struct View<'source, Element: Deserialize> {
    label: &'source Element,
    value: &'source [u8],
}

13. PhantomData 👻

Tweet

PhantomData has a dead-simple definition, but fascinating use-cases. PhantomData makes it look like your type contains another type even if it really doesn’t. It’s zero-sized, so it costs you nothing to use!

PhantomData can be useful when interfacing with external resources:

struct ExternalResource<T> {
    _marker: PhantomData<T>,
}

impl<T> ExternalResource<T> {
    fn fetch(&self) -> T { /* ... */ }
    fn update(&self, v: T) { /* ... */ }
}

If you’re working with FFI or otherwise need to manually restrict lifetimes:

struct FP<'a>(usize, *const u8, PhantomData<&'a ()>);

impl<'a> From<&'a Vec<u8>> for FP<'a> {
    fn from(v: &'a Vec<u8>) -> Self {
        FP(v.len(), v.as_ptr(), PhantomData)
    }
}

Playground
More in Nomicon.

12. Strings are collections

Tweet

A String is a collection that can be built from an iterator:

let my_string = vec![1, 2, 3, 4]
    .iter()
    .map(|i| (i * 2).to_string())
    .collect::<String>();

assert_eq!(my_string, "2468");

11. Enforce Clippy lints in a workspace

Update: As of Rust 1.74, Clippy lints can now be configured in Cargo.toml. While the method originally described in this tip is still available, Cargo.toml is likely a more convenient way to configure lints.

[lints.clippy]
large_digit_groups = "warn"

Tweet

Enforce a consistent set of Clippy lints across multiple crates in one workspace by adding to .cargo/config.toml at the project root.

[target.'cfg(all())']
rustflags = [
  "-Wclippy::large_digit_groups",
]

(Otherwise you’d need to have a #![warn(...)] directive in every crate.)

10. The never type !

Tweet

The never type in Rust (denoted by !) represents a type that will never exist. Called the “bottom type” in type theory, it’s used as the type of expressions that never resolve (e.g. panics or infinite loops) or the type of a return or break statement.

The never type can be coerced to any type. For example:

let x: u32 = panic!();

! type documentation
Standard library documentation
Infallible documentation
Nothing in Rust

9. Recursive declarative macros

Tweet

Build quick-and-dirty parsers with recursive declarative macros.

macro_rules! m {
    (y) => { true };
    (n) => { false };
    ($a:tt xor $($b:tt)+) => { m!($a) != m!($($b)+) };
}

assert!(m!(y xor n xor n));

Tweet

Link to a module, struct, or enum directly in rustdoc comments with the shorthand syntax:

/// Link to [`MyStruct`]

Works for external crates too!

/// Link to [`serde::Serialize`]

(Links to https://docs.rs/serde/latest/serde/ser/trait.Serialize.html.) Backticks are optional, they just format it nicely.

7. More precise access control

Tweet

Specify item visibility with the pub(restricted) syntax:

SyntaxVisible in…
pub(crate)current crate
pub(super)parent module
pub(self)current module
pub(in <path>)ancestor module

Docs

6. Reuse code in build.rs with #[path]

Tweet

Easily reuse code from your project in the build.rs file by using the #[path] directive:

#[path = "./src/parse.rs"]
mod parse;

fn main() {
    parse::om_nom(/* ... */);
}

Feature flags work in build.rs too!

#[cfg(feature = "parser")]
let feature_gated = /* ... */;

build.rs documentation
The #[path] attribute

5. Reduce unnecessary allocations with cows 🐮

Tweet

The clone-on-write smart pointer std::borrow::Cow creates owned values only when necessary. It’s useful when you want to work with both owned and borrowed values, and it can help you dynamically prevent unnecessary allocations.

fn f(v: &mut Cow<str>, m: bool) {
    if m {
        v.to_mut().push_str(", world");
    }
}

let mut v: Cow<str> = "hi".into();
f(&mut v, false);
assert!(v.is_borrowed()); // note: is_borrowed() is only available on nightly
f(&mut v, true);
assert!(v.is_owned());

Playground

4. Additional trait bounds per-function

Tweet

Did you know you can add additional trait bounds to a type parameter for a single function when writing traits or impl blocks?

trait MyTrait<T: Debug> {
    fn action(&self) {
        // Implemented when T is Debug
    }

    fn print(&self) where T: Display {
        // Only implemented when T is Debug + Display
    }
}

Playground

3. Use the AsRef trait to convert references

Tweet

Vec<T>? 😢
AsRef<[T]>? 😄

fn sum_is_even(v: impl AsRef<[u32]>) -> bool {
    v.as_ref().iter().fold(true, |e, i| e == (i & 1 == 0))
}

// both work!
sum_is_even(vec![0, 2, 4]));
sum_is_even([1]);

Playground
Docs

2. Use syn and quote when writing procedural macros

Tweet

The crates syn and quote make parsing and generating streams of Rust tokens a breeze—super handy when writing procedural macros!

More about writing procedural macros in Rust.

1. Use the dbg!(...) macro for better debugging

Tweet

Still using println!() debugging in Rust?

Use dbg!() instead. It prints the filename, line number, expression, and value and returns the value it was given. It’s in the standard library, and it’s shorter than println!().

println!("{value:?}");
my_function(&value);

vs.

my_function(dbg!(&value));

I’m a software engineer for NEAR Protocol and a graduate student at the Tokyo Institute of Technology.

Connect with me on Twitter and Mastodon.