Stream Concepts Overview
This section introduces Capy’s stream concepts—the abstractions that enable data to flow through your programs.
Prerequisites
-
Completed Buffer Sequences
-
Understanding of buffer sequences and the DynamicBuffer concept
Six Concepts for Data Flow
Capy defines six concepts for I/O operations, organized in three pairs:
| Concept | Direction | Description |
|---|---|---|
|
Read |
Partial reads—returns whatever is available |
|
Write |
Partial writes—writes as much as possible |
|
Read |
Complete reads—fills buffer or signals EOF |
|
Write |
Complete writes with explicit EOF signaling |
|
Read |
Callee-owns-buffers read pattern |
|
Write |
Callee-owns-buffers write pattern |
Streams vs Sources/Sinks
Streams: Partial I/O
Stream operations transfer some data and return. They do not guarantee a specific amount:
// ReadStream: may return fewer bytes than buffer can hold
auto [ec, n] = co_await stream.read_some(buffer);
// n might be 1, might be 1000, might be buffer_size(buffer)
// WriteStream: may write fewer bytes than provided
auto [ec, n] = co_await stream.write_some(buffers);
// n might be less than buffer_size(buffers)
This matches raw OS behavior—syscalls return when data is available, not when buffers are full.
Sources/Sinks: Complete I/O
Source/sink operations complete fully or signal completion:
// ReadSource: fills buffer completely, or returns EOF/error with partial
auto [ec, n] = co_await source.read(buffer);
// n == buffer_size(buffer), or ec indicates why not
// WriteSink: writes all data, with explicit EOF
co_await sink.write(buffers, true); // true = EOF after this
These are higher-level abstractions built on streams.
Buffer Sources/Sinks: Callee-Owns-Buffers
The third pair inverts buffer ownership:
-
With streams/sources/sinks, the caller provides buffers
-
With buffer sources/sinks, the callee provides buffers
// BufferSource: callee provides read-only buffers
const_buffer bufs[8];
auto [ec, count] = co_await source.pull(bufs, 8);
// bufs[0..count-1] now point to source's internal data
// BufferSink: callee provides writable buffers
mutable_buffer bufs[8];
std::size_t count = sink.prepare(bufs, 8);
// Write into bufs[0..count-1], then commit
co_await sink.commit(bytes_written);
This pattern enables zero-copy I/O—data never moves through intermediate buffers.
Type-Erasing Wrappers
Each concept has a corresponding type-erasing wrapper:
| Concept | Wrapper |
|---|---|
|
|
|
|
(Both) |
|
|
|
|
|
|
|
|
|
These wrappers enable:
-
APIs independent of concrete transport
-
Compilation firewalls (fast incremental builds)
-
Runtime polymorphism without virtual inheritance in user code
Choosing the Right Abstraction
Use Streams When:
-
You need raw, unbuffered I/O
-
You’re implementing a protocol that processes data incrementally
-
Performance is critical and you want minimal abstraction
The Value Proposition
Type-erased wrappers let you write transport-agnostic code:
// This function works with any stream implementation
task<> echo(any_stream& stream)
{
char buf[1024];
for (;;)
{
auto [ec, n] = co_await stream.read_some(mutable_buffer(buf));
if (ec.failed())
co_return;
auto [wec, wn] = co_await write(stream, const_buffer(buf, n));
if (wec.failed())
co_return;
}
}
The caller decides the concrete implementation:
// Works with Corosio TCP sockets
any_stream s1{tcp_socket};
echo(s1);
// Works with TLS streams
any_stream s2{tls_stream};
echo(s2);
// Works with test mocks
any_stream s3{test::stream{}};
echo(s3);
Same code, different transports—compile once, link anywhere.
Continue to Streams (Partial I/O) to learn the ReadStream and WriteStream concepts in detail.