Last updated on December 22, 2024.
A Review of Rust in 2024: What Next?
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
let mps: MyPackedStruct = MyPackedStruct ;
// 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 ; // 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.
/// As you can see, we're allowed to use floats in `const`!
const
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)]
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!
/// A type that needs to provide a cached value to callers.
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: = new;
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:
;
/// I wish that I could be like the cool kids
;
/// generic to types that impl `MyCoolTrait`
That last line there gives you the following error:
Checking rs_2024_article_codeblocks v0.1.0
error: tell the user what's going on
-/diagnostics_on_unimpl.rs:32:27
|
32 | func_with_cool_bounds; // 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 ` ` is not implemented for `UncoolType`
= note: You may wish to add ` on the affected item.
= note: If that's not an option, consider using `PartialCool` instead.
= help: the trait ` ` is implemented for ` `
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 = to_string_pretty.inspect_err?;
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:
;
;
async
Note that these aren't yet fully functional, as traits that use it are no longer dyn
compatible (new term for "object safe").
leads to this error:
error: the trait ` Fart` cannot be made into an object
-/afit.rs:45:25
|
45 |
| ^^^^^^^^ ` Fart` cannot be made into an object
|
note: for a
So, if you need to use trait objects (dyn Farts
syntax), you'll want to add a helper crate: async_trait
!
use async_trait;
Now, take_farter
compiles just fine! :D
Behind the scenes, though, this proc macro is doing a lot of work:
// ...
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;
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:
- zero memory (
core::mem::zeroed::<T>()
) is nowconst
!- skirts the transmute
core::slice::chunk_by
and friends- these methods are a godsend for parsing
use<'a>
bounds (capture syntax)- these let you better specify your lifetimes when using
impl Trait
syntax - I still don't recommend this syntax in libraries due to difficulties with semantic versioning compatibility. however, it feels great in your binaries!
- these let you better specify your lifetimes when using
c"my c string"
syntax to define c string literals- automatic
nul
termination - very useful in certain contexts.
- automatic
- IP address stuff in
core
- this is another thing that was just... gone on embedded
rustdoc
improvements- Documentation now mentions
dyn
compatibility on items /
to search- Items aren't duplicated in searches
- makes preludes feel less disgusting
- You can hide bars when they're in the way
- top 1 change of 2024 for ADHD
- Search for traits' associated types
- helps you avoid clicking that "source" button... so alluring... 🤤
- Documentation now mentions
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
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 enum
s 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:
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 Ok;
if let Ok = my_result
&& res > 2024_u32
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:
- To Save C, We Must Save ABI by JeanHeyd Meneide (phantomderp)
- C Isn't A Programming Language Anymore by Aria Desires (gankra)
- Pair Your Compilers at the ABI Café by Aria Desires
- The
glibc
s390
ABI Break by Jonathan Corbet on LWN.net
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: = account.username.inspect_none;
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)
These invariants on references are mentioned here in the RFC. Note that they're incomplete, so additional invariants may exist.