BufWriter

Introduction

xpp::io::BufWriter<W> wraps any AsyncWriter with an internal 8KB buffer. Small writes fill the buffer; when it's full, pending data is automatically flushed to the inner writer. Large writes (≥ 8KB) bypass the buffer entirely. Reduces per-call syscall overhead for message-building over TCP.

Satisfies the AsyncWriter concept — composable with io::copy. Takes ownership of the inner writer via move semantics. Works with .await(), co_await (C++20), or .then() chains (C++11).

IMPORTANT: flush() must be called explicitly before dropping. The destructor does NOT flush un-sent data — matching Rust's BufWriter behavior.

Example — .await()

#include <xpp/io/buf_writer.h>

xpp::EventLoop loop;
xpp::WaitScope scope(loop);

auto conn = xpp::net::TcpStream::connect("127.0.0.1:9090").await().unwrap();
xpp::io::BufWriter<xpp::net::TcpStream> buf(std::move(conn));

// Small writes accumulate — no syscalls yet
buf.write("HTTP/1.0 200 OK\r\n", 17).await();
buf.write("Content-Length: 5\r\n\r\n", 22).await();
buf.write("hello", 5).await();

// Must flush before dropping!
buf.flush().await();

Example — co_await (C++20)

auto conn = xpp::net::TcpStream::connect("127.0.0.1:9090").await().unwrap();
xpp::io::BufWriter<xpp::net::TcpStream> buf(std::move(conn));

co_await buf.write("HTTP/1.0 200 OK\r\n", 17);
co_await buf.write("Content-Length: 5\r\n\r\n", 22);
co_await buf.write("hello", 5);
co_await buf.flush();

How it works

write(buf, len)
    │
    ├── len ≥ 8KB? → flush pending + inner.write(buf, len) suspension
    │
    ├── buffer would overflow? → flush pending first
    │
    └── memcpy to m_buf, advance m_pos

flush()
    └── m_pos > 0? → inner.write(m_buf, m_pos) suspension → m_pos = 0

~BufWriter()
    └── does NOT flush (Rust behavior)

Small writes accumulate in the internal buffer with zero I/O. When the buffer would overflow, pending data is automatically flushed first, then the new data is buffered. Large writes (≥ 8KB) flush any pending data, then write directly to the inner writer — avoiding double-buffering.

The destructor does NOT call flush(). This matches Rust's behavior: the caller is responsible for flushing. If flush() is forgotten, data is silently discarded.

API Reference

MethodReturnsDescription
BufWriter(W writer)Take ownership of writer. Move-only
write(buf, len)Promise<ssize_t>Buffered write. May trigger auto-flush
flush()Promise<void>Send all buffered data to inner writer
inner()W&Access the inner writer (e.g., for close())

Usage Examples

Build HTTP response — .await()

xpp::io::BufWriter<xpp::net::TcpStream> buf(std::move(conn));

buf.write("HTTP/1.0 200 OK\r\n", 17).await();
buf.write("Content-Type: text/plain\r\n", 26).await();
buf.write("Content-Length: 5\r\n", 20).await();
buf.write("\r\n", 2).await();
buf.write("hello", 5).await();
buf.flush().await();  // Must flush — bytes still in buffer

Build HTTP response — co_await (C++20)

xpp::Promise<void> respond(xpp::net::TcpStream conn) {
    xpp::io::BufWriter<xpp::net::TcpStream> buf(std::move(conn));
    co_await buf.write("HTTP/1.0 200 OK\r\n", 17);
    co_await buf.write("Content-Type: text/plain\r\n", 26);
    co_await buf.write("Content-Length: 5\r\n", 20);
    co_await buf.write("\r\n", 2);
    co_await buf.write("hello", 5);
    co_await buf.flush();
}

Large bypass — .await()

xpp::io::BufWriter<xpp::net::TcpStream> buf(std::move(conn));

// Small header — buffered
buf.write("data: ", 6).await();

// Large body — bypasses buffer
buf.write(data, len).await();  // writes ≥ 8KB go directly to inner writer
buf.flush().await();

Large bypass — co_await (C++20)

xpp::Promise<void> upload(xpp::net::TcpStream conn, const void *data, size_t len) {
    xpp::io::BufWriter<xpp::net::TcpStream> buf(std::move(conn));
    co_await buf.write("data: ", 6);
    co_await buf.write(data, len);
    co_await buf.flush();
}

Close after flush

// .await()
buf.flush().await();
buf.inner().close();  // close the underlying TcpStream

// co_await (C++20)
co_await buf.flush();
buf.inner().close();