std::stream
This module provides a Web Streams-inspired API
for byte streams designed to work well with Silk’s async/task model.
The core goals of the Supported forms are:
- Ergonomic piping between producers and consumers.
- Backpressure via bounded buffering.
- Safe chunk ownership across tasks using an owned
Bytestype (no borrowed slice lifetime hazards).
Runtime note (Supported forms):
ReadableStream.read()/WritableStream.write()are blocking OS-thread operations implemented with mutex/condvar primitives.
See also:
- concurrency (tasks,
yield, structured blocks) - sync (mutex/condvar/channel; same blocking baseline)
Exported API#
module std::stream;
import std::arrays;
import std::buffer;
import std::interfaces;
import std::memory;
import std::result;
export enum StreamErrorKind {
OutOfMemory,
InvalidInput,
Closed,
Full,
Cancelled,
Aborted,
RuntimeFailed,
Unknown,
}
export error StreamFailed {
code: int,
}
impl StreamFailed {
public fn kind (self: &StreamFailed) -> StreamErrorKind;
}
// Owned, immutable byte chunks.
export struct Bytes {
handle: u64,
}
export type BytesResult = std::result::Result(Bytes, std::memory::AllocFailed);
impl Bytes {
public fn empty () -> Bytes;
public fn from_handle (handle: u64) -> Bytes;
public fn as_slice (self: &Bytes) -> std::arrays::ByteSlice;
public fn copy_from (slice: std::arrays::ByteSlice) -> BytesResult;
public fn copy_from_string (s: string) -> BytesResult;
public fn take_from_buffer (mut buf: &std::buffer::BufferU8) -> BytesResult;
}
impl Bytes as std::interfaces::Len {
public fn len (self: &Bytes) -> i64;
}
impl Bytes as std::interfaces::IsEmpty {
public fn is_empty (self: &Bytes) -> bool;
}
impl Bytes as std::interfaces::Sized {
public fn size (self: &Bytes) -> usize;
}
// Read outcomes.
export enum Read {
Done,
Pending,
// Bytes are transferred as raw handles; wrap with `Bytes.from_handle(handle)`.
Chunk(u64),
}
export type ReadResult = std::result::Result(Read, StreamFailed);
// Readable end.
export struct ReadableStream {
handle: u64,
}
impl ReadableStream {
public fn invalid () -> ReadableStream;
public fn is_valid (self: &ReadableStream) -> bool;
public fn cap (self: &ReadableStream) -> int;
public fn len (self: &ReadableStream) -> int;
public fn is_closed (self: &ReadableStream) -> bool;
public fn read (self: &ReadableStream) -> ReadResult;
public fn try_read (self: &ReadableStream) -> ReadResult;
public fn cancel (self: &ReadableStream) -> void;
public fn destroy (mut self: &ReadableStream) -> void;
}
// Writable end.
export struct WritableStream {
handle: u64,
}
impl WritableStream {
public fn invalid () -> WritableStream;
public fn is_valid (self: &WritableStream) -> bool;
public fn cap (self: &WritableStream) -> int;
public fn len (self: &WritableStream) -> int;
public fn is_closed (self: &WritableStream) -> bool;
public fn write (self: &WritableStream, mut chunk: Bytes) -> StreamFailed?;
public fn try_write (self: &WritableStream, mut chunk: Bytes) -> StreamFailed?;
public fn close (self: &WritableStream) -> void;
public fn abort (self: &WritableStream, err: StreamFailed) -> void;
public fn destroy (mut self: &WritableStream) -> void;
}
// A paired in-memory stream (writable → readable).
export struct PassThroughStream {
readable: ReadableStream,
writable: WritableStream,
}
export type PassThroughResult = std::result::Result(PassThroughStream, StreamFailed);
impl PassThroughStream {
public fn init_default () -> PassThroughResult;
public fn init (cap: int) -> PassThroughResult;
public fn take_readable (mut self: &PassThroughStream) -> ReadableStream;
public fn take_writable (mut self: &PassThroughStream) -> WritableStream;
}
// Transformer output.
export type TransformBytesResult = std::result::Result(Bytes, StreamFailed);
// A paired transform stage.
export struct TransformStream {
readable: ReadableStream,
writable: WritableStream,
transform_readable: ReadableStream,
transform_writable: WritableStream,
}
export type TransformResult = std::result::Result(TransformStream, StreamFailed);
impl TransformStream {
public fn init_default () -> TransformResult;
public fn init (cap: int) -> TransformResult;
public fn init_with_caps (cap_in: int, cap_out: int) -> TransformResult;
public fn take_readable (mut self: &TransformStream) -> ReadableStream;
public fn take_writable (mut self: &TransformStream) -> WritableStream;
public fn take_transform_readable (mut self: &TransformStream) -> ReadableStream;
public fn take_transform_writable (mut self: &TransformStream) -> WritableStream;
}
// Pipe a readable into a writable until done.
export fn pipe_to (mut src: ReadableStream, mut dst: WritableStream) -> std::result::Result(int, StreamFailed);
// Pipe until done or until aborted.
export fn pipe_to_abortable (
mut src: ReadableStream,
mut dst: WritableStream,
sig: std::abort_controller::AbortSignalBorrow?
) -> std::result::Result(int, StreamFailed);
Semantics#
Backpressure#
Each stream has a bounded in-memory queue. The cap is expressed in chunks
(Bytes values), not bytes:
PassThroughStream.init(cap)/TransformStream.init(cap)requirecap > 0(otherwise they returnErr(StreamFailed)withkind() == InvalidInput).WritableStream.write()blocks while the queue is full.ReadableStream.read()blocks while the queue is empty (until closed or errored).
Close vs cancel vs abort#
WritableStream.close():- graceful end-of-stream,
- readers drain remaining buffered chunks and then observe
Read::Done. ReadableStream.cancel():- marks the stream cancelled,
- discards buffered chunks,
- causes writers to fail with a
Cancellederror. WritableStream.abort(err):- marks the stream aborted with
err, - discards buffered chunks,
- causes readers to return
Err(err)fromread/try_read.
Transform streams#
TransformStream models a Web Streams-style transform stage.
In Silk currently, std::stream does not attach a transformer
callback internally. Instead, TransformStream exposes two bounded pipes and
expects you to run the transform loop in a task:
- input pipe: producers write to
writable, transformer reads fromtransform_readable, - output pipe: transformer writes to
transform_writable, consumers read fromreadable.
This design composes naturally with task-based structured concurrency: run the
transform loop in a task and rely on backpressure to bound memory.
Usage patterns#
Producer → consumer (tasks)#
import std::stream;
task fn producer (w: std::stream::WritableStream) -> int { ... }
task fn consumer (r: std::stream::ReadableStream) -> int { ... }
async fn main () -> int {
task {
match (std::stream::PassThroughStream.init_default()) {
Ok(stream) => {
let mut pt: std::stream::PassThroughStream = stream;
let w = pt.take_writable();
let r = pt.take_readable();
let hp = producer(w);
let hc = consumer(r);
let rp: int = yield hp;
let rc: int = yield hc;
if rp != 0 { return rp; }
if rc != 0 { return rc; }
return 0;
},
Err(_) => {
return 1;
},
}
}
}
Transform stage (tasks)#
TransformStream is a pair of pipes intended to be driven by a transformer task.
Typical wiring:
- producer writes to
take_writable(), - transformer reads from
take_transform_readable()and writes totake_transform_writable(), - consumer reads from
take_readable().
File I/O adapters (std::fs)#
std::fs provides task-based helpers for piping files into/out of streams:
import std::fs;
import std::fs::stream;
import std::stream;
async fn main () -> int {
task {
match (std::stream::PassThroughStream.init(2)) {
Ok(stream) => {
let mut pt: std::stream::PassThroughStream = stream;
let w = pt.take_writable();
let r = pt.take_readable();
let hr = std::fs::stream::pipe_file_to_stream("input.txt", w, 4096);
let hw = std::fs::stream::pipe_stream_to_file(r, "output.txt", 420);
let rr: std::stream::PipeResult = yield hr;
let rw: std::stream::PipeResult = yield hw;
match (rr) {
Err(_) => { return 2; },
}
match (rw) {
Err(_) => { return 3; },
}
return 0;
},
Err(_) => {
return 1;
},
}
}
}
Piping#
pipe_to is a structured copy loop:
- closes
dstwhensrcends, - aborts/cancels on error.
To make piping cooperatively cancellable, use pipe_to_abortable with an
std::abort_controller::AbortSignalBorrow. In the Supported forms, aborts are
observed between read/write steps; they do not yet interrupt a blocking
ReadableStream.read() call.
import std::stream;
fn run_pipeline (src: std::stream::ReadableStream, dst: std::stream::WritableStream) -> int {
let r = std::stream::pipe_to(src, dst);
return match (r) {
Ok(_) => 0,
Err(_) => 1,
};
}
Notes#
- The Supported forms uses blocking primitives; it is intended to become suspension-friendly once the async runtime exists.
- Drop semantics are designed to avoid leaked pipes:
- dropping
ReadableStreamcancels the stream (writers start failing), - dropping
WritableStreamcloses the stream (readers observeRead::Doneafter draining). PassThroughStream.take_readable/take_writableexist to make ownership transfer ergonomic in the Supported forms (moving out of struct fields is limited).
Source repository · Edit this page · View Markdown