Skip to content

Implement true-zero-heap inline single-item buffer for scalar next()/write_one() (Miri-gated) — follow-up to #1 #5

Description

@avrabe

Self-contained implementation ticket (for a dedicated agent). Entirely within this fork's async runtime; no other repo involved. Follows #1, whose perf/async-stream-amortize-single-item-alloc branch already made next()/write_one() amortized zero-alloc (reused heap buffer). This ticket is the remaining step: make the scalar single-item path true zero-heap (no allocator dependency at all) — the piece the no_std/MCU line needs (meld#299, gale#89).

Goal

For RawStreamReader::next / RawStreamWriter::write_one, when O::native_abi_matches_canonical_abi() (scalar payloads: u32/f32/…), use an inline [MaybeUninit<O::Payload>; 1] buffer instead of a heap Vec, so the path performs zero heap allocations including the first call and has no global-allocator dependency.

Why it's sound (do NOT skip — this is the load-bearing property)

The single-item buffer is moved into a WaitableOperation held across await. waitable.rs guarantees the op is PhantomPinned + Pin<&mut Self> and its destructor synchronously cancels the in-flight canon op before the memory is freed (in_progress_cancel "must synchronously complete… can block"; waitable.rs ~182-190). Therefore the runtime cannot reference the buffer after the future's Drop returns, so an inline buffer dropped after that synchronous cancel is not a use-after-free. Any implementation MUST preserve this ordering (cancel-then-free).

Scope limit (honest)

Only the scalar (native_abi_matches_canonical_abi) path can be zero-heap. When canonical ≠ native, AbiBuffer::new always allocates a separate Cleanup buffer for the lowered form (abi_buffer.rs:44-65) — non-scalar single-item streams keep one heap alloc regardless. Gate the inline path strictly on native_abi_matches_canonical_abi(); fall back to the existing reused-heap path otherwise.

Design

  1. AbiBuffer storage becomes an enum: Heap(Vec<MaybeUninit<T>>) (today) vs Inline([MaybeUninit<T>; 1]) — OR a parallel SingleAbiBuffer used only by next/write_one. abi_ptr_and_len/advance/remaining operate on &mut [MaybeUninit<T>] either way; into_vec is unsupported on the inline variant (these callers never call it — they pop() the single item).
  2. The inline storage lives in the WaitableOperation (future frame) — sound per the cancel property above; the self.next_buf/one_buf heap Vecs are no longer needed for the scalar path.
  3. Preserve the existing Vec-based multi-item API (read/write/write_all/collect) unchanged.

Oracle (required gates)

  • Strengthen next_zero_alloc_in_steady_state / write_one_zero_alloc_in_steady_state to assert zero allocations including the first call (true-no-heap counter) for a scalar payload.
  • Miri in CI over the new path — this catches the cancellation/Pin use-after-free that the functional test cannot. Must not merge without the Miri gate green (the failure mode is UB, not a wrong value).

Refs

#1 (amortization predecessor), meld#299, gale#89 (the no-grow MCU chain this unblocks at the async layer).

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions