Transfer Algorithms
This section explains the composed read/write operations and transfer algorithms.
Prerequisites
-
Completed Buffer Sources and Sinks
-
Understanding of all six stream concepts
Composed Read/Write
The partial operations (read_some, write_some) often require looping. Capy provides composed operations that handle the loops for you.
read
Fills a buffer completely by looping read_some:
#include <boost/capy/read.hpp>
template<ReadStream Stream, MutableBufferSequence Buffers>
io_task<std::size_t>
read(Stream& stream, Buffers buffers);
Await-returns io_result<std::size_t>, destructuring as [ec, n]. Keeps reading until:
-
Buffer is full (
n == buffer_size(buffers)) -
The underlying
read_somereports a condition before the buffer is full (the condition is propagated with the partial count)
Example:
char buf[1024];
auto [ec, n] = co_await read(stream, make_buffer(buf));
// n == 1024, or ec indicates why not
read_at_least
A straightforward extension of read: instead of filling buffers
completely, it stops as soon as at least n bytes have been read:
#include <boost/capy/read_at_least.hpp>
template<ReadStream Stream, MutableBufferSequence Buffers>
io_task<std::size_t>
read_at_least(Stream& stream, Buffers buffers, std::size_t n);
The required amount n must be met or exceeded; the remaining capacity of
buffers is optional, so a single read_some that delivers more than n
bytes keeps the extra without looping again. Keeps reading until:
-
At least
nbytes have been read (n <= return value <= buffer_size(buffers)) -
The underlying
read_somereports a condition beforenbytes are read (the condition is propagated with the partial count)
If n exceeds buffer_size(buffers) the request is impossible to satisfy and
the operation fails immediately with std::errc::invalid_argument and a count
of 0, without reading.
Example:
char buf[4096];
// Require 16 bytes; opportunistically take whatever else is ready.
auto [ec, n] = co_await read_at_least(stream, make_buffer(buf), 16);
// n >= 16 on success, possibly up to 4096
write
Writes all data by looping write_some:
#include <boost/capy/write.hpp>
template<WriteStream Stream, ConstBufferSequence Buffers>
io_task<std::size_t>
write(Stream& stream, Buffers buffers);
Keeps writing until:
-
All data is written (
n == buffer_size(buffers)) -
Error occurs (returns error with partial count)
Example:
co_await write(stream, make_buffer("Hello, World!"));
write_at_least
The mirror of read_at_least, provided for symmetry: it stops as soon as at
least n bytes have been written, rather than draining buffers entirely:
#include <boost/capy/write_at_least.hpp>
template<WriteStream Stream, ConstBufferSequence Buffers>
io_task<std::size_t>
write_at_least(Stream& stream, Buffers buffers, std::size_t n);
Keeps writing until:
-
At least
nbytes have been written (n <= return value <= buffer_size(buffers)) -
The underlying
write_somereports a condition beforenbytes are written (the condition is propagated with the partial count)
If n exceeds buffer_size(buffers) the operation fails immediately with
std::errc::invalid_argument and a count of 0, without writing.
write_now
write_now eagerly writes a complete buffer sequence, attempting to finish
synchronously. If every underlying write_some completes without suspending,
the whole operation completes in await_ready with no coroutine suspension.
It caches a single coroutine frame and reuses it across calls, avoiding
repeated allocation on a hot write path:
#include <boost/capy/io/write_now.hpp>
template<WriteStream Stream>
class write_now;
Construct it from a stream, then call it like a function. Only one operation may be outstanding at a time:
write_now wn(stream);
auto [ec, n] = co_await wn(make_buffer("hello"));
if (ec)
throw std::system_error(ec);
Transfer Algorithms
Transfer algorithms move data between sources/sinks and streams.
push_to
Transfers data from a BufferSource to a destination:
#include <boost/capy/io/push_to.hpp>
// To WriteSink (with EOF propagation)
template<BufferSource Source, WriteSink Sink>
io_task<std::size_t>
push_to(Source& source, Sink& sink);
// To WriteStream (streaming, no EOF)
template<BufferSource Source, WriteStream Stream>
io_task<std::size_t>
push_to(Source& source, Stream& stream);
The source provides buffers via pull(). Data is pushed to the destination. Buffer ownership stays with the source: no intermediate copying when possible.
Example:
// Transfer file to network
mmap_source file("large_file.bin");
co_await push_to(file, socket);
pull_from
Transfers data from a source to a BufferSink:
#include <boost/capy/io/pull_from.hpp>
// From ReadSource
template<ReadSource Source, BufferSink Sink>
io_task<std::size_t>
pull_from(Source& source, Sink& sink);
// From ReadStream (streaming)
template<ReadStream Stream, BufferSink Sink>
io_task<std::size_t>
pull_from(Stream& stream, Sink& sink);
The sink provides writable buffers via prepare(). Data is pulled from the source directly into the sink’s buffers.
Example:
// Receive network data into compression buffer
compression_sink compressor;
co_await pull_from(socket, compressor);
Why No buffer-to-buffer?
There is no push_to(BufferSource, BufferSink) because it would require redundant copying. The source owns read-only buffers; the sink owns writable buffers. Transferring between them would need an intermediate copy, defeating the zero-copy purpose.
Instead, compose with an intermediate stage:
// Transform: BufferSource -> processing -> BufferSink
task<> process_pipeline(any_buffer_source& source, any_buffer_sink& sink)
{
const_buffer src_arr[8];
while (true)
{
auto [ec, src_bufs] = co_await source.pull(src_arr);
if (ec == cond::eof)
break;
std::size_t consumed = 0;
for (auto const& b : src_bufs)
{
auto processed = transform(b);
// Write processed data to sink
mutable_buffer dst_arr[8];
auto dst_bufs = sink.prepare(dst_arr);
std::size_t copied = buffer_copy(
dst_bufs,
make_buffer(processed));
co_await sink.commit(copied);
consumed += b.size();
}
source.consume(consumed);
}
co_await sink.commit_eof(0);
}
Naming Convention
The algorithm names reflect buffer ownership:
| Name | Meaning |
|---|---|
|
Source provides buffers → push data to destination |
|
Sink provides buffers → pull data from source |
The preposition indicates the direction of buffer provision, not data flow.
Error Handling
All transfer algorithms return (error_code, std::size_t):
-
error_code— Success, EOF, or error condition -
std::size_t— Total bytes transferred before return
On error, partial transfer may have occurred. The returned count indicates how much was transferred.
Example:
auto [ec, total] = co_await push_to(source, sink);
if (ec == cond::eof)
{
// Normal completion
std::cout << "Transferred " << total << " bytes\n";
}
else if (ec)
{
// Error occurred
std::cerr << "Error after " << total << " bytes: " << ec.message() << "\n";
}
Reference
| Header | Description |
|---|---|
|
Composed read operations |
|
Read at least a minimum number of bytes |
|
Composed write operations |
|
Write at least a minimum number of bytes |
|
Eager write with frame caching |
|
BufferSource → WriteSink/WriteStream transfer |
|
ReadSource/ReadStream → BufferSink transfer |
You have now learned about transfer algorithms. Continue to Physical Isolation to learn how type erasure enables compilation firewalls.