Rust Static Assertions

11 minute read

Four months ago I published the static_assertions crate. To understand how it works, we need to learn how to creatively use compile errors.

Here I’ll be explaining how one would go about defining macros to assert constant conditions and assert equal sized types at compile-time.

Setup

This post assumes some basic knowledge of Rust and it’ll be using version 1.21.0. This code should work with some previous and all later versions.

Rust 1.21.0 can be installed and enabled via:

rustup install 1.21.0 && rustup default 1.21.0

If you’d rather not install Rust, you can follow along just as easily with a Rust playground.

Asserting Constant Conditions

The final result can be used here.

Reasoning

The main reason I wrote static_assertions was to have something similar to static_assert in C++ but available in Rust.

One common error in programming is integer overflow/underflow. What does Rust tell us when this happens within a constant?

const ASSERT: usize = 0 - 1;
warning: constant evaluation error: attempt to subtract with overflow.
This will become a HARD ERROR in the future
 --> src/main.rs:1:18
  |
1 | const ASSERT: usize = 0 - 1;
  |                       ^^^^^
  |
  = note: #[warn(const_err)] on by default

Well, we want it to be a hard error now. This can be achieved with:

#[deny(const_err)]
const ASSERT: usize = 0 - 1;
error: constant evaluation error: attempt to subtract with overflow.
This will become a HARD ERROR in the future
 --> src/main.rs:2:18
  |
2 | const ASSERT: usize = 0 - 1;
  |                       ^^^^^
  |
note: lint level defined here
 --> src/main.rs:1:8
  |
1 | #[deny(const_err)]
  |        ^^^^^^^^^

error: aborting due to previous error

Note: You may be getting warnings about unused code. We can use #[allow(dead_code)] to disable these warnings.

Better!

Now how can we turn this into an assertion? Recall that bool can be converted into an integer primitive via an as cast.

In Rust, false as usize == 0 and true as usize == 1. Thus we can easily update our code to accept a boolean:

const CONDITION: bool = false;

#[deny(const_err)]
#[allow(dead_code)]
const ASSERT: usize = 0 - !CONDITION as usize;
error: constant evaluation error: attempt to subtract with overflow.
This will become a HARD ERROR in the future
 --> src/main.rs:4:23
  |
5 | const ASSERT: usize = 0 - !CONDITION as usize;
  |                       ^^^^^^^^^^^^^^^^^^^^^^^
  |
note: lint level defined here
 --> src/main.rs:3:8
  |
3 | #[deny(const_err)]
  |        ^^^^^^^^^

error: aborting due to previous error

The Macro

We can define our initial macro as such:

macro_rules! const_assert {
    ($condition:expr) => {
        #[deny(const_err)]
        #[allow(dead_code)]
        const ASSERT: usize = 0 - !$condition as usize;
    }
}

Here $condition is our input and we let Rust know we’re accepting it as any expression as denoted by :expr.

Let’s put our new macro to use with a simple boolean:

const_assert!(false);
error: constant evaluation error: attempt to subtract with overflow.
This will become a HARD ERROR in the future
 --> src/main.rs:5:31
  |
5 |         const ASSERT: usize = 0 - !$condition as usize;
  |                               ^^^^^^^^^^^^^^^^^^^^^^^^
...
9 | const_assert!(false);
  | --------------------- in this macro invocation
  |
note: lint level defined here
 --> src/main.rs:3:16
  |
3 |         #[deny(const_err)]
  |                ^^^^^^^^^
...
9 | const_assert!(false);
  | --------------------- in this macro invocation

error: aborting due to previous error

Awesome, it works! Now let’s try asserting a more useful condition.

const_assert!(2 + 2 == 4);

This compiles perfectly. If we change == to != or make the expression false in any way, it’ll give us a compile error just like before.

Let’s try asserting multiple conditions.

const_assert!(2 + 2 == 4);

const_assert!(5 * 5 == 25);
error[E0428]: the name `ASSERT` is defined multiple times
 --> src/main.rs:5:9
  |
5 |         const ASSERT: usize = 0 - !$condition as usize;
  |         -----------------------------------------------
  |         |
  |         `ASSERT` redefined here
  |         previous definition of the value `ASSERT` here
...
9 | const_assert!(2 + 2 == 4);
  | -------------------------- in this macro invocation
  |
  = note: `ASSERT` must be defined only once in the value namespace of this module

error: aborting due to previous error

Uh-oh.

Well Rust allows us to use _ to create anonymous bindings with let. What if we used that here? Within the macro, rename ASSERT to _ as such:

const _: usize = 0 - !$condition as usize;
error: expected identifier, found `_`
 --> src/main.rs:5:15
  |
5 |         const _: usize = 0 - !$condition as usize;
  |               ^
...
9 | const_assert!(2 + 2 == 4);
  | -------------------------- in this macro invocation
  |
  = note: `_` is a wildcard pattern, not an identifier

Hmmm. So it seems that Rust doesn’t support anonymous constants. We could try using a let binding instead of const.

let _: usize = 0 - !$condition as usize;
error: expected item after attributes
 --> src/main.rs:4:27
  |
4 |         #[allow(dead_code)]
  |                           ^
...
9 | const_assert!(2 + 2 == 4);
  | -------------------------- in this macro invocation

error: macro expansion ignores token `let` and any following
 --> src/main.rs:5:9
  |
5 |         let _: usize = 0 - !$condition as usize;
  |         ^^^
  |
note: caused by the macro expansion here; the usage of `const_assert!`
is likely invalid in item context
 --> src/main.rs:9:1
  |
9 | const_assert!(2 + 2 == 4);
  | ^^^^^^^^^^^^^^^^^^^^^^^^^^

Aaahh!! What does this mean??

If you’ve been doing what I’ve been doing and have been using const_assert! within a global scope you’ll get this error. What if we put it into main (or some other function)?

fn main() {
    const_assert!(2 + 2 == 4);
    const_assert!(5 * 5 == 25);
}

Wow, it compiles! Sweet!

And because we’re using _, we can have multiple assertions at once.

Improving Usability

What if we don’t want to define a function every time we need to use this macro? Fortunately, there’s a solution. I call it “labeling”.

Macros can accept expressions, but they can also accept other items such as identifiers. We can take advantage of this and have our macro accept an optional identifier to “label” a global assertion.

macro_rules! const_assert {
    ($condition:expr) => {
        #[deny(const_err)]
        #[allow(dead_code)]
        let _: usize = 0 - !$condition as usize;
    }; // <-- Notice the semicolon!
    ($label:ident; $condition:expr) => {
        fn $label() {
            const_assert!($condition);
        }
    };
}

We have now defined a macro with two sets of input options. The second set will create a function with the given label as its name. It’ll then recursively call const_assert! with the provided expression.

Let’s put it to use in both global and function contexts:

const_assert!(simple_math; 2 * 2 == 2 + 2);

fn main() {
    const_assert!(2 + 2 == 4);
    const_assert!(5 * 5 == 25);
}

It compiles! However, we get this warning:

warning: function is never used: `simple_math`
  --> src/main.rs:8:9
   |
8  | /         fn $label() {
9  | |             const_assert!($condition);
10 | |         }
   | |_________^
...
14 |   const_assert!(simple_math; 2 * 2 == 2 + 2);
   |   ------------------------------------------- in this macro invocation
   |
   = note: #[warn(dead_code)] on by default

We can disable the warning by annotating our fn with #[allow(dead_code)]. Now it’ll successfully build without any warnings or errors.

What if we want to use a different convention for our labels? For example:

const_assert!(SimpleMath; 2 * 2 == 2 + 2);

We can annotate our label function with #[allow(non_snake_case)]. We can actually combine our annotations into one line:

($label:ident; $condition:expr) => {
    #[allow(non_snake_case, dead_code)]
    fn $label() {
        const_assert!($condition);
    }
};

Ensuring Constants Only

But wait, because we’re using a let binding, our macro can also accept non-constants. Fortunately there’s a trick to ensure const_assert! only accepts constants. We can do so by making our value be an array size.

In Rust, arrays are declared as [T; N] where N is some constant usize.

We can update our let binding accordingly:

let _ = [(); 0 - !$condition as usize];

Here our array element type is the unit type: a zero sized type. The array’s size is exactly the same as our previous value.

We can now remove the #[deny(const_err)] annotation since array sizes are never allowed to overflow. We may also remove #[allow(dead_code)] while we’re at it since our variable is _ which can’t be used anyway.

Now our macro has finally grown up and looks like this:

macro_rules! const_assert {
    ($condition:expr) => {
        let _ = [(); 0 - !$condition as usize];
    };
    ($label:ident; $condition:expr) => {
        #[allow(non_snake_case, dead_code)]
        fn $label() {
            const_assert!($condition);
        }
    };
}

Better Usability (Advanced)

What if we want to assert multiple conditions? Sure we can use && but maybe we want to make our conditions be independent yet required.

My approach is to make the conditions comma-separated.

macro_rules! const_assert {
    ($($condition:expr),+ $(,)*) => {
        let _ = [(); 0 - !($($condition)&&+) as usize];
    };
    ($label:ident; $($rest:tt)+) => {
        #[allow(non_snake_case, dead_code)]
        fn $label() {
            const_assert!($($rest)+);
        }
    };
}

Here the first branch takes one or more comma-separated expressions as well as zero or more trailing commas. The second branch takes all tokens passed after $label and forwards them to the first branch.

When repeating $condition we can place && between each repetition since it’s viewed as a single token.

This new definition can be used as such:

const_assert! { simple_math;
    2 * 2 == 2 + 2,
    5 * 2 == 10,
}

Asserting Equal Sized Types

The final result can be used here.

Reasoning

Rust is a language that’s suitable for working on systems at a low level. This may involve pointer casts between different types.

If we change our types’ internal structures, their sizes may also change. This will lead to strange behavior if we’re accidentally casting between differently-sized types.

What if we want to ensure usize has the same size as u64 within somewhere deep within our code? Maybe we can use something in conjunction with #[cfg(target_pointer_width = "64")].

One way of performing type casts is via std::mem::transmute. It takes the bits of a value of a given type and returns the same bits but with a different type.

When the output type is not the same size as the input, we get a compile error:

use std::mem::transmute;

fn main() {
    let a: u32 = 42;
    let b: u16 = unsafe { transmute(a) };
}
error[E0512]: transmute called with types of different sizes
 --> src/main.rs:5:27
  |
5 |     let b: u16 = unsafe { transmute(a) };
  |                           ^^^^^^^^^
  |
  = note: source type: u32 (32 bits)
  = note: target type: u16 (16 bits)

error: aborting due to previous error

Can we use this to our advantage in a general and ergonomic way? Of course!

This is Rust; we shouldn’t have to be unsafe to get what we want. As it turns out, getting the function pointer will emit the same error.

let f = transmute::<u32, u16>;

You may have noticed the odd ::<> after transmute. This is unofficially referred to as turbofish syntax. It tells the compiler what generic parameters to use for transmute, which takes an input and output type.

The Macro

With these tools we can write our basic macro:

use std::mem::transmute;

macro_rules! assert_eq_size {
    ($x:ty, $y:ty) => {
        let _ = transmute::<$x, $y>;
    }
}

Here $x and $y are annotated with :ty, which means that we can accept any type. Yes, Rust’s macros are this powerful.

Let’s try this out:

assert_eq_size!(u16, u32);
error[E0512]: transmute called with types of different sizes
  --> src/main.rs:5:17
   |
5  |         let _ = transmute::<$x, $y>;
   |                 ^^^^^^^^^^^^^^^^^^^
...
10 |     assert_eq_size!(u16, u32);
   |     -------------------------- in this macro invocation
   |
   = note: source type: u16 (16 bits)
   = note: target type: u32 (32 bits)

error: aborting due to previous error

Excellent, it works!

Improving Usability

Similar to with how we initially defined const_assert!, we can only use this macro within the context of a function. We can apply the same labeling technique here!

macro_rules! assert_eq_size {
    ($x:ty, $y:ty) => {
        let _ = transmute::<$x, $y>;
    }; // <-- Notice the semicolon!
    ($label:ident; $x:ty, $y:ty) => {
        #[allow(non_snake_case)] // Allows any naming convention
        #[allow(dead_code)]      // Can be left unused
        fn $label() {
            assert_eq_size!($x, $y);
        }
    };
}

This new definition can take a “label” and create a function that then recursively calls assert_eq_size! with the given types.

We can now use our macro in both global and function contexts:

assert_eq_size!(string_size; &str, (usize, usize));

fn main() {
    assert_eq_size!([u8; 4], u32);
}

Better Usability (Advanced)

We’re not done yet; we can make our macro even more awesome!

What if we want to check more than two types against one another? Yeah, that’s very much possible.

macro_rules! assert_eq_size {
    ($x:ty, $($xs:ty),+ $(,)*) => {
        $(let _ = transmute::<$x, $xs>;)+
    };
    ($label:ident; $($rest:tt)+) => {
        #[allow(dead_code, non_snake_case)]
        fn $label() {
            assert_eq_size!($($rest)+);
        }
    };
}

Here the first branch takes some first type $x followed by one or more comma-separated types denoted by $xs. It can also have zero or more trailing commas. The second branch takes all tokens passed after $label and forwards them to the first branch.

Notice that we’re repeating our statement over each type matched by $xs but we maintain the same initial $x type.

Our final macro can now be used as such:

assert_eq_size! { some_sizes;
    [u8; 4],
    (u16, u16),
    u32,
}

Final Comments

Evidently we can utilize Rust’s powerful compile-time checks in a very elegant way.

This post was inspired from speaking to other Rustaceans at the Boston Rust meetup last night. Apparently there’s members of the community who are just as interested in these cool little hacks as I am. 😁

Currently, assert_eq_size! can’t be implemented via const_assert! because std::mem::size_of is not a constant function (yet).

Check out static_assertions for more functionality just like this! If you have any suggestions feel free to open an issue or pull request.