Rust is a programming language with a highly active community. Contributors are constantly adding new features and working toward new goals. This article summarizes my favorite features added in 2024, and also addresses my hopes for the future!

If you're here to see me complain about what we don't have yet, please head to the Wishlist for 2025 section.

Review of 2024

The Rust project has made countless improvements to the language this year. Let's review and see what might come next!

&raw Reference Syntax

We now support creating &raw const and &raw mut references as distinct types. These let you safely refer to fields without a well-defined alignment, much like the long-time workarounds (addr_of! and addr_of_mut! macros) did:

/// These fields will be "packed", so there won't be extra padding.
///
/// This can reduce memory usage, but screws with everything else. It's
/// sometimes used in low-level contexts.
///
/// See: https://doc.rust-lang.org/nomicon/other-reprs.html#reprpacked
#[repr(packed)]
struct MyPackedStruct {
    field_a: i32,
    field_b: i8,
    field_c: u16,
}

let mps: MyPackedStruct = MyPackedStruct {
    field_a: 582,
    field_b: -4,
    field_c: 989,
};

// scary: this will probably cause undefined behavior (UB)!
//
// the compiler now gives you an error here.
let bad: *const i32 = &mps.field_a as *const i32;

// happy: no problems here.
let good: *const i32 = &raw const mps.field_a;

// we'll want to read the value out using this method.
//
// note: it's only unsafe because `read_unaligned` doesn't care if the
// type is `Copy`. so you can do some nonsense with it
// ...kinda like `core::mem::replace`
let value: i32 = unsafe { good.read_unaligned() }; // this is how you'd read the value

Again, though, avoid Packed representations if you can. They're a bit of a footgun. If you do need to use them, though, &raw is vital!

At first, their usage might seem unclear. What is the difference if the syntax just spits out a *const or *mut... just like as casting would? In short, Rust usually requires you to first create a reference (&MyType) before you can cast to a raw pointer (*const MyType and *mut MyType).

However, Rust's references have certain guarantees that raw references don't have. In particular, they must be both aligned and dereferenceable.1 When these aren't true, you immediately create opportunities for undefined behavior (UB) by even compiling the thing. Miscompilations are likely due to LLVM's reliance on those two invariants.

Raw reference (&raw) syntax addresses these problems by telling LLVM that those invariants might not be true. Certain optimizations (and other reliant invariants) are now turned off or adjusted.

Floating-Point Types in const fn

In the past, you may have tried to use floating-point (FP) numbers within const functions. However, before Rust 1.82, the compiler would stop you. This limitation stemmed from platform differences in FP numbers.

To understand why, you need to know a bit of context. In Rust, const refers to more than something that won't change - it's a block that can be computed at compile-time! This system spares many runtime operations, making programs faster. However, since FP numbers have platform differences, it's harder to compute that stuff at compile-time. If you do, your program's behavior will change depending on what machine compiled it, even if the cross-compilation is expected to be deterministic!

There's also another problem. If you want to avoid those cross-compilation flaws, you have to write rules for floats to follow at compile-time. They should be very close to runtime behavior, and ideally, exactly the same. Notably, Go fell into this trap, causing major differences in behavior depending on when floats are evaluated. Every time you use floats in Go, you have to ensure all your code agrees.

With these requirements in mind, and a lot of hard work, Rust has introduced floats in const fn! It uses many custom rules to specify exactly how they should work. These are given in RFC 3514: Float Semantics, which specifies how floating-point numbers should work in the language.

struct Maybe {
    pub float: f32,
}

/// As you can see, we're allowed to use floats in `const`!
const fn float_in_const(call_me: &Maybe) -> (bool, f32) {
    let f: f32 = call_me.float; // also in your data structures :)

    let new = f / 1.1;
    (new.is_finite(), new)
}

Note that most methods on the f32/f64 primitives don't yet use this. For example, f32::powf and f32::powi aren't yet const. Using #![feature(const_float_methods)] on Nightly can get you some of the way there, though these power functions don't seem to be included yet.

#[expect(lint)]

These attributes are just like #[allow(lint)], but they also give an error when the "expectation" isn't satisfied.

For example, if you put #[allow(unused)] onto a function, but later start calling it somewhere, you typically wouldn't notice the change. You may forget the function is used in your API. The #[expect] attribute doesn't let this happen - it'll show an error if you violate its expectation.

// you can just replace `#[allow(lint)]` with `#[expect(lint)]`
//
// #[allow(unused)]
#[expect(unused)]
type SomeUnusedItem = i32;

This has already fixed some bugs in my code, so I wholeheartedly suggest giving it a try!

core::error::Error Trait Stabilization (error in core)

If you've been in the embedded trenches before 1.81, you've seen Issue #103765: Tracking Issue for Error in core.

Everyone and their mother was using the (now defunct) #![feature(error_in_core)] attribute on their crate - and they all had to use Nightly to boot.

This is no longer a problem! anyhow, thiserror, and my rip-off crate, pisserror all support embedded usage of Error now, at least through no_std! Note that anyhow still requires some form of allocator.

Anyways... I feel like framing this link on my wall. https://doc.rust-lang.org/stable/core/error/index.html

LazyCell and LazyLock

These two types are upstreamed from the well-known once_cell crate, but the standard library is finally catching up!

LazyCell is the standard library's version of the once_cell::unsync::Lazy type. It can't be used across threads or in statics, but it's made for something else: initializing a variable only when it's needed! They're typically used when you need to run a large computation once, then use the cached results.

In comparison to OnceCell, LazyCell is used when the computation is always the same. You can only specify the "creation function" in the constructor.

/// A huge type that we need for our app!
struct BigType {
    creation_time: Instant,
    // lots of other fields...
}

impl BigType {
    /// pretend this takes forever. we'll use `sleep` to get the point across :)
    fn new(creation_time: Instant) -> Self {
        std::thread::sleep(Duration::from_millis(500));
        Self { creation_time }
    }
}

/// A type that needs to provide a cached value to callers.
struct SomethingWithCache {
    cache: LazyCell<BigType>,
}

impl SomethingWithCache {
    pub fn new() -> Self {
        Self {
            cache: LazyCell::new(|| BigType::new(Instant::now())),
        }
    }

    fn big_type(&self) -> &BigType {
        // this deref will initialize the type if not done already!
        //
        // otherwise, we'll just use the cached value...
        &*self.cache
    }
}

On the other hand, LazyLock (once_cell::sync::Lazy) is often used on servers and in other high-performance scenarios. They work with concurrency and threading, and you'll also tend to find them inside static variables. These are a bit slower than LazyCell, but offer greater flexibility.

/// Here's a static, which is accessible throughout the program.
///
/// Let's pretend that creating it takes a looooong time...
static BIG_SCARY_VARIABLE: LazyLock<BigType> = LazyLock::new(|| BigType::new(Instant::now()));

struct BigType {
    creation_time: Instant,
}

impl BigType {
    fn new(creation_time: Instant) -> Self { /* ... */ }
}

By the way, you may have noticed that you don't need to have any mutability to initialize these types. You can mutate them from behind a shared reference, as they use unsafe behind the scenes to mutate themselves.

When it lands on Stable, the lazy_get Nightly feature will also allow you to replace the Lazy types' internal values with your own.

Anyways, these types have always been around in one way or another. But now, you don't need to use an external crate!

The #[diagnostic::on_unimplemented] Attribute

This simple attribute is extremely influential - it lets you create your own compile errors for the user to see, all without a proc macro! Here's how it works:

#[diagnostic::on_unimplemented(
    message = "tell the user what's going on",
    label = "oh hey im pointing at the failed code",
    note = "You may wish to add `#[derive(Cool)] on the affected item.",
    note = "If that's not an option, consider using `PartialCool` instead." // in my bevy era
)]
trait MyCoolTrait<'a> {
    fn buf(&self) -> &'a [u8];
}

struct CoolType<'data>(&'data [u8]);

impl<'data> MyCoolTrait<'data> for CoolType<'data> {
    fn buf(&self) -> &'data [u8] {
        self.0
    }
}

/// I wish that I could be like the cool kids
struct UncoolType;

/// generic to types that impl `MyCoolTrait`
fn func_with_cool_bounds<'data, Cool: MyCoolTrait<'data>>(cool_type: Cool) {
    println!("dang look at all this data: {:#?}", cool_type.buf())
}

fn main() {
    let cool_type: CoolType = CoolType(&[1, 2, 3]);
    let uncool_type: UncoolType = UncoolType;

    func_with_cool_bounds(cool_type); // all good. compiler is happy
    func_with_cool_bounds(uncool_type); // uh oh! but hey, a custom err message...
}

That last line there gives you the following error:

    Checking rs_2024_article_codeblocks v0.1.0 (/Users/barrett/Downloads/rs_2024_article_codeblocks)
error[E0277]: tell the user what's going on
  --> src/diagnostics_on_unimpl.rs:32:27
   |
32 |     func_with_cool_bounds(uncool_type); // uh oh! but hey, a custom err ...
   |     --------------------- ^^^^^^^^^^^ oh hey im pointing at the failed code
   |     |
   |     required by a bound introduced by this call
   |
   = help: the trait `MyCoolTrait<'_>` is not implemented for `UncoolType`
   = note: You may wish to add `#[derive(Cool)] on the affected item.
   = note: If that's not an option, consider using `PartialCool` instead.
   = help: the trait `MyCoolTrait<'data>` is implemented for `CoolType<'data>`

This attribute is vital for making certain types of derive macros. Please give it a try if you maintain a crate relying heavily on traits, as this technique can seriously help to inform your users!

ABI Documentation

It's in a weird module, but under the primitive fn (function pointer, NOT Fn trait) module documentation, there is now a section on ABI compatibility!

These can help a lot when relying on #[repr(Rust)] types. These docs seem most useful when writing alternative compilers (like mrustc, gccrs, or Dozer), helping folks to start on the advanced intricacies of rustc instead of getting stuck on small ABI differences.

(as a note, please support those projects I listed. alternative compilers are essential to the Rust ecosystem's continued development!)

Option::inspect, Result::inspect, and Result::inspect_err

I'm in love with these methods. The two inspect methods are great for logging parsing progression, and Result::inspect_err feels almost vital at this point for logging on errors:

let json: String = serde_json::to_string_pretty(report).inspect_err(|e| {
    tracing::warn!("Failed to make report into a pretty JSON string. (err: {e})")
})?;

I enjoy these so much that, in a few projects, I bumped up my MSRV just to use them. They make your code so nice to read...

core::ptr::from_ref::<T> and core::ptr::from_mut::<T>

These types, tracked in Issue #106116, are a great way to create raw pointers in the general case. They protect from the usual annoyances of as casting, where you can slightly bend the type system if not careful.

If you use these types, please consider linting for an accidental swap of shared (&) and exclusive (&mut) references. See clippy::as_ptr_cast_mut for more info.

Return-Position impl Trait... in Traits (RPITIT)

It feels like those acronyms get longer each time I look. In any case, with Rust 1.75, traits can now use RPIT like any other function/method item.

These work just like you'd expect, so please see the announcement blog post for additional information.

Async Functions in Traits (AFIT)

The last PR also added async functions to traits, though they're a little knee-capped. Here's what that can look like:

pub trait Fart {
    async fn fart(&self) {
        tokio::time::sleep(std::time::Duration::from_millis(self.get_fart_time().await)).await;
        println!("<fart>");
    }

    async fn get_fart_time(&self) -> u64;
}

struct Bob;

impl Bob {
    const FART_TIME_MS: u64 = 300_u64;
}

impl Fart for Bob {
    async fn get_fart_time(&self) -> u64 {
        Self::FART_TIME_MS
    }
}

struct Sam;

impl Sam {
    const FART_TIME_MS: u64 = 600_u64; // much longer
}

impl Fart for Sam {
    async fn get_fart_time(&self) -> u64 {
        Self::FART_TIME_MS
    }
}

async fn main() {
    let bob = Bob;
    let sam = Sam;

    tokio::join! {
        bob.fart(),
        sam.fart()
    };
}

Note that these aren't yet fully functional, as traits that use it are no longer dyn compatible (new term for "object safe").

fn take_farter(farter: &dyn Fart) {}

leads to this error:

error[E0038]: the trait `afit::Fart` cannot be made into an object
  --> src/afit.rs:45:25
   |
45 | fn take_farter(farter: &dyn Fart) {}
   |                         ^^^^^^^^ `afit::Fart` cannot be made into an object
   |
note: for a trait to be "dyn-compatible" it needs to allow building a vtable to allow the call to be resolvable dynamically; for more information visit <https://doc.rust-lang.org/reference/items/traits.html#object-safety>
  --> src/afit.rs:4:14
   |
3  | pub trait Fart {
   |           ---- this trait cannot be made into an object...
4  |     async fn fart(&self) {
   |              ^^^^ ...because method `fart` is `async`
...
9  |     async fn get_fart_time(&self) -> u64;
   |              ^^^^^^^^^^^^^ ...because method `get_fart_time` is `async`
   = help: consider moving `fart` to another trait
   = help: consider moving `get_fart_time` to another trait
   = help: the following types implement the trait, consider defining an enum where each variant holds one of these types, implementing `afit::Fart` for this new enum and using it instead:
             afit::Bob
             afit::Sam

So, if you need to use trait objects (dyn Farts syntax), you'll want to add a helper crate: async_trait!

use async_trait::async_trait;

#[async_trait]
pub trait Fart { /* ... */ }

#[async_trait]
impl Fart for Bob { /* ... */ }

#[async_trait]
impl Fart for Sam { /* ... */ }

Now, take_farter compiles just fine! :D

Behind the scenes, though, this proc macro is doing a lot of work:

impl Fart for Bob {
    fn get_fart_time<'life0, 'async_trait>(
        &'life0 self,
    ) -> Pin<Box<dyn Future<Output = u64> + Send + 'async_trait>>
    where
        'life0: 'async_trait,
        Self: 'async_trait,
    {
        Box::pin(async move {
            if let Some(__ret) = None::<u64> {
                #[allow(unreachable_code)]
                return __ret;
            }
            let __self = self;
            let __ret: u64 = { Self::FART_TIME_MS };
            #[allow(unreachable_code)]
            __ret
        })
    }
}
    // ...

See that Box right there? That's a peek into the embedded trenches...

Nonetheless, this option is useful for binaries, but be careful when doing this stuff in your libraries. Additional changes are needed for semantic versioning to be consistent here.

const Blocks

When you need these, you need them. const evaluation has historically been a little difficult to control, governed by the internal (opaque) rules of the compiler as it pursues const promotion. In libraries operating in low-level spaces, const eval can significantly impact performance, so many folks pursue it aggressively: if a maintainer has any doubt, they'll const-ify any parameter into a const PARAM just to encourage the compiler.

With const blocks, you can directly tell the compiler that it should simplify the given expression at compile-time.

Here's a short example of how this looks:

// probably not realistic but shhh pretend we're talking to an allocator
let m = allocate(const { 1024 * 8 });

If there was any doubt whether that would be evaluated by the compiler, it's gone now. Our troubles were dealt with at compile-time.

Some Extras

Here are some other things I liked:

Wishlist for 2025

Ok, 2024 was great for Rust! But, there are still some things that are missing. Let's discuss my wishlist for Rust in 2025:

Compile-Time Reflection

Compile-time reflection is a construct to analyze source code at compile time. In short, it replaces small code generation tasks (think serde, thiserror, and bevy_reflect) with normal Rust source code.

In my view, this is one of the few large-scale optimizations on compile time we've got left (you know... ignoring the whole batch compiler thing). It would vastly reduce compile times for the largest Rust binaries, especially for large applications like web servers.

Reflection would lessen the amount of syn we'd see slowly compiling alone, allowing Rust developers to iteratively make changes as if we hand-rolled all our serde::De/Serialize implementations, without giving up on our high-level constructs. It is my #1 prospect for the language - after this, everyone could go home until 2026. I would still be happy. (please don't though!)

Modern Allocator Trait

Let's take a look at my favorite thing ever - the new Allocator trait's allocate() method:

pub unsafe trait Allocator {
    fn allocate(&self, layout: Layout) -> Result<NonNull<[u8]>, AllocError>;

    // ...
}

Ok... do you see that? The Result in its return type?

This new allocator interface lets you fallibly manage your memory without checking for null pointers at every stage! Or, in other words, sane human beings can manage the memory in their applications without immediately resorting to unsafe. Drop is Rust's comfy free, but allocate is finally giving Rust a comfy malloc.

This new allocator will be very impactful! I'll list a few benefits here:

  • The Linux kernel can use Rusty memory management (i.e. unite kernel::alloc with... everyone else)
  • Embedded developers won't have to fight demons to manage their allocators
  • Crates using custom allocators for performance won't have to use global state
  • Stuff will get faster in general :)

Unfortunately, it's not done yet. If you have any ideas or needs that seem unfulfilled, please reach out to the Allocators WG (working group) on Zulip!

Enum Variant Types

When you write an enum, you sometimes want to pass around a variant for various reasons. Maybe it avoids dozens of newtypes, powers your state machine, or helps in reducing boilerplate.

Unfortunately, Rust's enums are not currently capable of these, as variants are not types. The workaround isn't pretty. I linked it above, but often, you'll end up using the newtype pattern on all of your enum variants:

pub enum ComponentDescription {
    CpuDescription(CpuDescription),
    RamDescription(RamDescription),
    // ...

That's because, without it, you can't share each variant as a type. For example, if I know that this component has a RamDescription, then there's no use in pattern matching it out. A lot of Rust code would become significantly easier to read with variant types.

Stabilization of #[feature(let_chains)]

I really love let_chains! With these, you can combine verbose instances of pattern matching into just a few lines.

let my_result: Result<u32, MyError> = Result::Ok(2025_u32);

if let Ok(res) = my_result
    && res > 2024_u32
{
    println!("ayo it's 2025!");
}

They're not currently stable, but I use them in all my Nightly projects! :)

ABI

I hope that #[repr(Rust)] never becomes stable. Read these for more info:

Oh... you came back! I didn't expect that!

So anyways, Rust is considering its own stable ABI called crabi, with its own repr tag: #[repr(crabi)]. In short, this means you'd be able to write languages that "spoke" Rust. I think we'd start seeing more high-level systems languages (similar to the now-defunct, and wonderful, June Language or Go) based on the crabi ABI model.

Python would likely gain support for crabi, so I can imagine a world where the two languages have a large overlap in ecosystems.

adt_const_params feature - Use Custom Types in Your const Generics

This one is nice. In essence, you can now share important info at compile-time without using const functions and parameters. These can encourage the compiler to evaluate related expressions at compile-time and avoid passing parameters around. Instead, it's engrained into the type system!

Option::inspect_none

This one sounds kinda funny, but I want a way to log when there's no value.

Like so:

let username: Option<String> = account.username().inspect_none(|| {
    tracing::error!("User does not have a username! (id: `{}`)", account.id())
});

Currently, we have to use if account.username().is_none(), which is a bit verbose for a logging construct.

Closing Thoughts

These are some of my favorite changes from 2024, and my hopes for 2025!

Rust is doing its Annual Community Survey until December 23rd, 2024, so please fill out the form if you want to share your thoughts! (but blog posts work too)

1

These invariants on references are mentioned here in the RFC. Note that they're incomplete, so additional invariants may exist.

Go Home