Skip to content

← Unsafe and Soundness step 24 of 24

Hard Framework

Build a ring buffer with MaybeUninit

Complete a fixed-capacity, double-ended ring buffer using Box<[MaybeUninit<T>]>. Keep the supplied ring_script wrapper, which exercises the buffer with owned strings.

Implement push_front, pop_back, get and Drop, and fix the unsafe read in pop_front. The allocation, capacity queries and push_back are supplied. The buffer must support non-Copy, non-Clone values without cloning them.

Method Required behavior
push_back(value) / push_front(value) Insert at that end and return true; return false when full
pop_front() / pop_back() Remove and return that end's value, or None when empty
get(i) Borrow the value at logical index i, or return None when i >= len
Drop Drop values still owned by the buffer, without reading unused slots

A rejected push consumes and drops its argument; this API returns a boolean, not the rejected value. Capacity may be zero or one. At zero capacity, pushes fail, pops and lookups return None, and no indexing or modulo is needed.

Track owned values, not just bytes

Maintain len <= capacity. When capacity is nonzero, head identifies the first live slot. Logical element i, for i < len, is stored at (head + i) % capacity.

The live window contains exactly the values owned by the buffer. Other slots must not be read or dropped as owned Ts. A popped slot may still contain the old bytes, but those bytes do not grant ownership of another value.

For capacity four, head = 3 and len = 2 means physical slots 3 and 0 are live, in that order. The next back insertion uses slot 1; a front insertion uses slot 2. This is why traversing 0..len as physical indices fails after wraparound.

Use MaybeUninit's operations deliberately

MaybeUninit<T> can hold storage without a valid T in it. Dropping the wrapper does not drop a contained T, so your buffer must manage that work. It has the same size and alignment as T; that does not make uninitialized storage safe to treat as a T.

  • write(value) initializes a slot without dropping its previous contents. Use it for an available slot; overwriting a live value would leak that value.
  • assume_init_read() reads an initialized value out. Treat this as an ownership transfer and remove that slot from the live window.
  • assume_init_ref() borrows an initialized value. The returned reference must remain valid for its borrow of the ring.
  • assume_init_drop() drops an initialized value in place. That value must not be dropped again.

The assume_init operations are unsafe because the caller must establish the relevant initialization, ownership and borrowing conditions. Add a SAFETY comment explaining those conditions at each unsafe block; the enabled Clippy lint checks for documentation, not whether the argument is correct.

The constructor creates each uninitialized slot separately. Repeating a slot with vec![MaybeUninit::uninit(); cap] would require a Clone implementation for MaybeUninit<T>, which is available when T: Copy. It cannot be used for this unconstrained generic buffer or for String.

Update the window around ownership changes

Guard full or empty cases before computing a slot. Avoid subtracting one directly from a zero usize head. For the exercise's small capacities, (head + capacity - 1) % capacity wraps backward; for unrestricted large capacities, also consider overflow in additions before the modulo.

Write an inserted value before publishing it through head and len. When popping, ensure the transferred value is no longer recorded as owned by the ring before anything can panic or call user code. The supplied front pop reads the value and then updates its indices without an intervening user call; do not add a panicking operation between those steps.

These pushes accept an already-created T: they do not clone it or grow the allocation. If a future API creates values through a closure, that introduces an additional panic boundary to account for.

Drop the live window

Reuse the removal logic to drain values still in the ring:

impl<T> Drop for Ring<T> {
    fn drop(&mut self) {
        while self.pop_front().is_some() {}
    }
}

With non-panicking element destructors, this releases each remaining value once, including when the ring is dropped during an unrelated unwind. Popped values belong to the caller and must not be dropped by the ring again.

This loop does not guarantee leak-free cleanup if a T::drop panics: the loop stops, and the remaining MaybeUninit slots do not drop their contents. A stronger cleanup guarantee requires additional unwind handling. A second panic during an existing unwind can abort the process.

Script behavior and verification limits

ring_script(cap, ops) returns one string per command:

Command Output
push_back S / push_front S "ok" or "full"
pop_front / pop_back Removed string or "none"
len Decimal length
join Live strings in front-to-back order, separated by commas
Unknown command "?"

Commands split at the first ASCII space. Pushes preserve the entire remaining text, including additional spaces; with no argument they push an empty string. join on an empty buffer returns an empty string.

The grader checks script output, compilation and clippy -D warnings. Long scripts and extra heap allocations may expose some memory errors, but they do not prove unsafe code sound or detect every leak. Review the live-window invariant and use values with observable destructors when testing ownership.