Skip to content

← Async From First Principles step 8 of 25

Hard Framework

`RawWaker` and `RawWakerVTable`: building a `Waker` with unsafe

Item 16.7 built a Waker from Arc<W> with no unsafe at all. That convenience is a thin wrapper over the representation every runtime actually uses, and this problem is that representation.

fn make_waker(state: Arc<Counter>) -> Waker
pub fn refcount_trace(clones: usize, wakes: usize) -> (usize, usize)

refcount_trace builds one Waker over a shared Counter, calls wake_by_ref wakes times, then clones the waker clones times and disposes of each clone — the even-indexed ones by wake() (which consumes), the odd-indexed ones by drop(). It returns (observed wake count, final strong count of the Arc). Get the refcounting right and the strong count is always exactly 2: the original state handle, plus the one owned by the raw pointer inside the surviving Waker.

What a Waker really is

Two words. A *const () data pointer and a &'static RawWakerVTable — and the vtable is four function pointers:

RawWakerVTable::new(clone, wake, wake_by_ref, drop)

That is why passing a Waker around is free, why it does not allocate, and why it is not a Box<dyn Fn()>. It is also why building one is unsafe: the compiler has no idea what your data pointer points at, so you are promising that all four functions agree about it.

The convention that makes it work is the one Arc already uses:

  • the data pointer is Arc::into_raw(state), and it owns one strong count;
  • clone must produce a second pointer that also owns one, so it increments;
  • wake consumes the count it was given — reconstruct the Arc and let it drop;
  • wake_by_ref must not consume it — reconstruct the Arc inside a ManuallyDrop so the count survives;
  • drop releases the count without doing any work.

Get clone wrong by one and you have a use-after-free. Get drop wrong by one and you have a leak. Neither is a compile error, and under -O neither is reliably a crash. That is the real lesson of this item: unsafe moves the proof obligation from the compiler to you, and nothing checks your work.

The safety contract, precisely

From the RawWaker docs: all four functions must be thread-safe, even though RawWaker is itself neither Send nor Sync. The reason is that Waker is Send + Sync, and it can be handed to another thread and invoked there through a shared reference. Arc‘s atomic counters are what make the usual implementation satisfy this; a Rc-based one would be unsound even though it compiles.

Rust 2024 makes you say unsafe twice

Since edition 2024, unsafe_op_in_unsafe_fn is on by default: marking a function unsafe no longer makes its whole body an implicit unsafe block. Declaring “callers must uphold X” and performing an unchecked operation are two separate acts, and you now write both. Inside each of your unsafe fn vtable entries you still need an explicit unsafe { .. } around the Arc::from_raw call.

The starter forgets the outer one and gets E0133: “call to unsafe function Waker::from_raw is unsafe and requires unsafe block”.

Pointer casts, idiomatically

ptr.cast::<Counter>() rather than ptr as *const Counter. Same machine code, and it cannot silently change mutability or discard a const the way an as cast can.

Which should you actually use?

Wake from item 16.7. Always, in real code. This item exists so that when you read tokio’s RawWakerVTable — or a no_std executor that cannot afford Arc — you recognise the shape and can check the refcount argument yourself.

Loading visualization…