Rust-1.93

#146 May 2026

146. char::MAX_LEN_UTF8 — Size UTF-8 Buffers Without Magic Numbers

Every time you’ve called char::encode_utf8, you’ve written [0u8; 4] from memory. Rust 1.93 stabilises char::MAX_LEN_UTF8 so you don’t have to keep that magic number in your head.

The magic number you keep typing

encode_utf8 writes the UTF-8 bytes of a char into a &mut [u8] and returns a &mut str pointing at the written portion. The slice has to be big enough — which means knowing that the worst-case UTF-8 encoding is 4 bytes:

1
2
3
let mut buf = [0u8; 4]; // why 4? because UTF-8, that's why
let s = '🦀'.encode_utf8(&mut buf);
assert_eq!(s, "🦀");

That 4 is correct but unexplained. Anyone reading your code has to either trust you or go re-derive the UTF-8 spec.

The named version

Rust 1.93 stabilises two constants on char:

1
2
assert_eq!(char::MAX_LEN_UTF8, 4);
assert_eq!(char::MAX_LEN_UTF16, 2);

MAX_LEN_UTF8 is the maximum number of u8s encode_utf8 can ever write. MAX_LEN_UTF16 is the same for encode_utf16 (a surrogate pair = 2 u16s). Drop them straight into your buffer declarations:

1
2
3
4
5
6
7
8
let mut buf = [0u8; char::MAX_LEN_UTF8];
let s = '🦀'.encode_utf8(&mut buf);
assert_eq!(s, "🦀");
assert_eq!(s.len(), 4);

let mut wide = [0u16; char::MAX_LEN_UTF16];
let w = '🦀'.encode_utf16(&mut wide);
assert_eq!(w.len(), 2);

Same behaviour, but the intent is self-documenting — the buffer is sized to hold exactly one char, by definition.

Sizing a buffer for N chars

Where this really pays off is when you’re computing a buffer for several chars on the stack:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
const N: usize = 8;
let mut buf = [0u8; N * char::MAX_LEN_UTF8];

let mut pos = 0;
for c in ['h', 'é', 'l', 'l', 'o'] {
    let s = c.encode_utf8(&mut buf[pos..]);
    pos += s.len();
}

assert_eq!(&buf[..pos], "héllo".as_bytes());

Now if Unicode ever expanded its scalar value range and MAX_LEN_UTF8 grew, your code would still be correct. With a hardcoded 4, you’d have a silent buffer overflow waiting to happen the day someone bumps the constant.

Why bother?

It’s a small change — one constant, no new behaviour. But it kills a real source of off-by-one bugs (people writing [0u8; 3] because they “only handle Latin-1”) and makes UTF-8 buffer code legible at a glance. Available since Rust 1.93 (January 2026).

#145 May 2026

145. Duration::from_nanos_u128 — Round-Trip Nanoseconds Without the u64 Cast

Duration::as_nanos() hands you a u128. Duration::from_nanos() takes a u64. You feed one into the other and the compiler yells at you — or worse, you cast and quietly truncate at 584 years. Rust 1.93 closed the loop with from_nanos_u128.

The mismatched-types papercut

The old API was asymmetric. Going from Duration to nanos was 128-bit:

1
2
3
4
5
use std::time::Duration;

let d = Duration::new(7, 250);
let n: u128 = d.as_nanos();
assert_eq!(n, 7_000_000_250);

Coming back, though, you only got from_nanos(_: u64) — so the round-trip needed a cast:

1
2
3
4
5
use std::time::Duration;

let n: u128 = Duration::new(7, 250).as_nanos();
let back = Duration::from_nanos(n as u64); // narrowing cast, fingers crossed
assert_eq!(back, Duration::new(7, 250));

That as u64 silently truncates anything past u64::MAX — and u64::MAX nanoseconds is roughly 584 years. Inside a calendar app you’ll never notice. Inside a scientific or simulation context, you absolutely will.

from_nanos_u128 matches as_nanos

Rust 1.93 stabilised Duration::from_nanos_u128, a const fn that takes the full 128-bit value:

1
2
3
4
5
use std::time::Duration;

let n: u128 = Duration::new(7, 250).as_nanos();
let back = Duration::from_nanos_u128(n);
assert_eq!(back, Duration::new(7, 250));

Same shape on both sides. No cast, no truncation, no silent wraparound.

Past the 584-year ceiling

Where the new constructor actually earns its keep is when you have nanoseconds counts that wouldn’t fit in a u64:

1
2
3
4
5
6
7
8
9
use std::time::Duration;

// 10^24 ns is ~31.7 million years — well past u64::MAX nanos
let nanos: u128 = 10_u128.pow(24) + 321;
let d = Duration::from_nanos_u128(nanos);

assert_eq!(d.as_secs(), 10_u64.pow(15));
assert_eq!(d.subsec_nanos(), 321);
assert_eq!(d.as_nanos(), nanos); // exact round-trip

Duration itself stores (u64 seconds, u32 nanos), so it has plenty of room — the old from_nanos was just bottlenecked by its argument type.

One thing to watch

from_nanos_u128 panics if you hand it more than Duration::MAX worth of nanoseconds. If you’re pulling values from user input or untrusted sources, guard the upper bound yourself — there isn’t a checked_from_nanos_u128 (yet).

When to reach for it

Use from_nanos_u128 whenever you already have a u128 of nanoseconds — typically because it came out of as_nanos, an arithmetic accumulator, or a high-precision external clock. Stick with the plain from_nanos(_: u64) for short-lived timeouts and durations measured in milliseconds or seconds; the u64 is plenty.

Stabilised in Rust 1.93 (January 2026). Available as const fn, so it works in const contexts too.

#144 May 2026

144. Vec::into_raw_parts — Hand a Vec to C Without the ManuallyDrop Dance

You want to give a Rust-allocated buffer to C and re-take it later. That means handing over (ptr, len, capacity) — and historically, prying those three out of a Vec without freeing the allocation meant wrapping the vector in ManuallyDrop first. Rust 1.93 stabilises Vec::into_raw_parts, a single safe call that returns the triple and consumes the Vec for you.

The pain: extracting parts while suppressing drop

The classic recipe leaks the Vec’s destructor on purpose so the C side owns the memory. You need three reads and a guard to keep Drop from racing the allocator:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
use std::mem::ManuallyDrop;

let v: Vec<u32> = vec![10, 20, 30];

let mut me = ManuallyDrop::new(v);
let ptr = me.as_mut_ptr();
let len = me.len();
let cap = me.capacity();

assert_eq!(unsafe { *ptr.add(1) }, 20);
assert_eq!((len, cap), (3, 3));

// Hand (ptr, len, cap) to C here.
// Reclaim it later with Vec::from_raw_parts to free the allocation.
let _reclaimed = unsafe { Vec::from_raw_parts(ptr, len, cap) };

It works, but the ManuallyDrop wrapper exists only to keep the destructor from running. Forget it, write mem::forget(v) in the wrong order, or read capacity() after the move and you’ve got a use-after-free or a leak.

The fix: one safe call, three return values

Vec::into_raw_parts(self) -> (*mut T, usize, usize) consumes the Vec, hands you the pointer-length-capacity triple, and leaves the allocation alive for you to manage:

1
2
3
4
5
6
7
8
9
let v: Vec<u32> = vec![10, 20, 30];
let (ptr, len, cap) = v.into_raw_parts();

assert_eq!((len, cap), (3, 3));
assert_eq!(unsafe { *ptr.add(1) }, 20);

// Reclaim and free at the end (or hand to C and have C call back).
let reclaimed = unsafe { Vec::from_raw_parts(ptr, len, cap) };
assert_eq!(reclaimed, vec![10, 20, 30]);

No wrapper, no separate field reads, no chance of accidentally calling a &self method after the move. The method is const, too.

String::into_raw_parts follows the same shape

String gets the same treatment in 1.93. The triple is (*mut u8, usize, usize), which is what String::from_raw_parts wants back:

1
2
3
4
5
6
7
let s = String::from("hello");
let (ptr, len, cap) = s.into_raw_parts();

assert_eq!((len, cap), (5, 5));

let rebuilt = unsafe { String::from_raw_parts(ptr, len, cap) };
assert_eq!(rebuilt, "hello");

The pairing is the point: into_raw_parts is safe (the Vec/String is gone, no aliasing exists yet), and from_raw_parts is unsafe (you’re asserting the triple came from a matching allocator with the right layout). The split keeps the unsafety where it actually lives.

When to reach for it

Any FFI boundary where the C side will hold the buffer for a while: graphics buffers, codec frames, command queues, anything with an extern "C" fn free_my_thing(ptr, len, cap) callback. Also handy when you’re building your own typed handles around a raw allocation — Box::into_raw covers the single-value case; into_raw_parts covers the variable-length one.

If you only need the pointer and nothing will ever reclaim the allocation, Vec::leak is still the shorter call. Reach for into_raw_parts the moment the capacity matters — i.e. anyone, anywhere, might want to give the memory back.

#140 May 2026

140. slice::as_array — Lock a Slice Into a Fixed-Size Array Reference

A function hands you &[u8] but the next step wants &[u8; 32]. The old answer was <&[u8; 32]>::try_from(slice) — a turbofish-and-trait dance for what is really just a length check. Rust 1.93 stabilises slice::as_array, the method-call version that does exactly that.

The pain: TryFrom for what should be a method

Cryptography APIs, parsers, and FFI all funnel data through &[T], but consumers usually want a concrete &[T; N] so they can index without bounds checks or pattern-match the fields out. The canonical conversion looks like this:

1
2
3
4
5
6
7
8
9
use std::convert::TryFrom;

fn fingerprint(digest: &[u8]) -> Result<&[u8; 32], &'static str> {
    <&[u8; 32]>::try_from(digest).map_err(|_| "expected 32 bytes")
}

let bytes = [0u8; 32];
assert!(fingerprint(&bytes[..]).is_ok());
assert!(fingerprint(&bytes[..16]).is_err());

It works, but every time you have to remember to write <&[u8; 32]>::try_from(...) with the reference inside the turbofish — write <[u8; 32]>::try_from instead and you’ll get an owned array (and a T: Clone bound) that you didn’t ask for. The error message when a coworker gets the form wrong is its own little adventure.

The fix: a method on [T] that returns Option<&[T; N]>

<[T]>::as_array::<N> is a plain method call. The length check is the same — the whole slice must be exactly N long — but the call site reads like every other slice method:

1
2
3
4
5
6
7
let bytes: &[u8] = &[0u8; 32];

let arr: Option<&[u8; 32]> = bytes.as_array();
assert!(arr.is_some());

let short: &[u8] = &bytes[..16];
assert!(short.as_array::<32>().is_none());

No TryFrom import, no angle-brackets-around-a-reference. The turbofish goes on the method, where it belongs, and the return type is Option — which is what you wanted anyway when the conversion can fail.

as_mut_array for the writeable side

The same trick works through &mut [T]. Useful when you’ve been handed a sub-slice of a buffer and want to write a fixed-size record into it without indexing each field:

1
2
3
4
5
let mut buf = [0u8; 16];
let header: &mut [u8; 4] = (&mut buf[..4]).as_mut_array().unwrap();
*header = *b"RIFF";

assert_eq!(&buf[..4], b"RIFF");

as_array and as_mut_array mirror <&[T; N]>::try_from / <&mut [T; N]>::try_from exactly — same semantics, fewer keystrokes, better error type.

Pair it with split_first_chunk for parsers

If you only need the first N bytes (and want to keep the rest), reach for split_first_chunk::<N> from the existing family. Use as_array when the slice is supposed to be exactly the right length — return values from a digest() call, decoded base64 of known size, FFI buffers carved to spec:

1
2
3
4
5
6
fn parse_ipv4(bytes: &[u8]) -> Option<[u8; 4]> {
    bytes.as_array::<4>().copied()
}

assert_eq!(parse_ipv4(&[192, 168, 1, 1]), Some([192, 168, 1, 1]));
assert_eq!(parse_ipv4(&[10, 0, 0]), None);

.copied() turns &[u8; 4] into [u8; 4] when you want an owned array — works because [u8; 4]: Copy.

When to reach for it

Anywhere you currently write <&[T; N]>::try_from(slice).ok() or slice.try_into().ok() and have to annotate the type to steer the inference. as_array is shorter, the failure case is an Option instead of a Result<_, TryFromSliceError>, and the turbofish sits where every other generic slice method puts it. Small ergonomics, but you’ll write it ten times this week.