Skip to content

streams: direct-access byte edges for host stream<u8> ends (A21, #128) - #226

Merged
lannbot merged 1 commit into
mainfrom
a21-direct-byte-edges
Aug 23, 2026
Merged

streams: direct-access byte edges for host stream<u8> ends (A21, #128)#226
lannbot merged 1 commit into
mainfrom
a21-direct-byte-edges

Conversation

@lannbot

@lannbot lannbot commented Aug 22, 2026

Copy link
Copy Markdown
Collaborator

Closes #128.

Contract (embedder-api amendment A21, wasmtime DirectSource/DirectDestination-shaped per the issue's ask): for stream<u8> only, StreamWriter.writeDirect(produce) and Stream.readDirect(consume) — plus the same forms on the low-level HostWritableEnd/HostReadableEnd seam — park a direct session. At every rendezvous with a peer operation of nonzero capacity the callback runs exactly once, synchronously, inside the rendezvous, with a scoped view of the peer's bytes (remaining() + markWritten/markRead). When the peer is a guest the view aliases guest linear memory, so an external byte mover's own set() is the single canonical-ABI copy — the one-copy floor the issue names, pinned by a view.buffer === memory.buffer test.

Design points, all in the amendment text:

  • "more"/"done" verdicts are wasmtime's poll cadence spelled event-style for JS's push-shaped producers; a session drains across many guest reads/writes with no re-park overhead.
  • Marks acknowledge on clean return only. "done" with zero marked retracts (the JS spelling of wasmtime's Pending: demand arrived while the ring was empty; the peer's op stays parked, no event). Zero-marked-"more" and throwing callbacks reject the session, discard the invocation's marks, and leave the peer parked and the stream alive — a zero-progress COMPLETED copy (unreachable in definitions.py for nonzero ops) is never emitted.
  • Scope is the validity window: the callback object dies on return (later calls throw, naming the rule); views re-derive per call, so memory.grow between rendezvous of one parked session is safe (pinned by test).
  • Zero-length-read readiness position (Concurrency.md): a parked direct session completes a probe immediately, callback not invoked — the armed session is the readiness claim.
  • Host↔host stays at the floor: writeDirect vs chunk read(max) delivers the scratch as the owned chunk; readDirect vs a parked chunk write views the offered chunk itself (A5 borrow, callback-scoped). Two direct sessions refuse loudly (neither owns memory).
  • A7/A8/A15 all inherited: PeerTrappedError with the delivered count (with A7's completed-before-trap precision via a low-level-seam-only out-param — the public signatures match the contract exactly), drop/cancel resolve the running total, the A15 transfer guard covers readDirect, one-in-flight-per-end covers the direct forms.
  • SAB-backed views work in both directions (the embedder performs the copy); pinned by test.

Implementation. The rendezvous half is a seam in task/streams.ts that collapses to the reference's dst.write(src.read(n)) verbatim when neither side is direct, and routes the no-copy outcomes (retraction/failure) onto branch shapes definitions.py already produces (the parked-side-exhausted and zero-capacity-arrival states). The session half — scope, mark accounting, verdict cadence, promise — lives in exec/host_streams.ts; the conventions layer mirrors write/read's refusal and peer-trap shapes. 31 new tests (25 raw-seam, 6 e2e through stream-pass).

Full just gates pass (incl. shells, browsers, smoke-tls, smoke-c0). No breaking labels: additive surface only (new methods + exported types); lockstep manifests untouched.

wasmtime DirectSource/DirectDestination-shaped. StreamWriter.writeDirect /
Stream.readDirect (and the low-level HostWritableEnd/HostReadableEnd forms)
park a direct session: at every rendezvous with a peer operation of nonzero
capacity the callback runs exactly once, synchronously, inside the
rendezvous, against a scoped view of the peer's bytes — guest linear memory
when the peer is a guest, so an external byte mover's own set() IS the
single canonical-ABI copy. "more"/"done" is wasmtime's poll cadence
spelled event-style; "done" with zero marked retracts (the speculative-park
correction), and marks acknowledge on clean return only, so no zero-progress
COMPLETED copy is ever emitted.

The rendezvous half is a seam in task/streams.ts that collapses to the
reference's dst.write(src.read(n)) verbatim whenever neither side is direct;
the no-copy outcomes route to branch shapes definitions.py already produces.
The session half (scope, mark accounting, verdict cadence, promise) lives in
exec/host_streams.ts. Host-to-host stays at the one-copy floor against the
chunk forms (scratch-becomes-chunk / view-of-offered-chunk); two direct
sessions refuse loudly. Zero-length probes complete without invoking the
callback — the armed session is the readiness claim.

Contract: embedder-api amendment A21; architecture §7 gains the scoped
exception to "views into guest memory are never exposed".
@lannbot
lannbot enabled auto-merge August 22, 2026 23:57
@lannbot
lannbot merged commit d9cc2c7 into main Aug 23, 2026
3 checks passed
@lann
lann deleted the a21-direct-byte-edges branch August 23, 2026 00:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

2 participants