System I/O Integration
Platform Buffer Structures
struct iovec {
void* iov_base; // Pointer to data
size_t iov_len; // Length of data
};
POSIX uses iovec with readv(), writev(), recvmsg(), and sendmsg(). Capy’s buffer types place the pointer first and the size second, matching iovec in both order and width. Filling an iovec is therefore a field-for-field copy, with no conversion.
|
Matching layout does not license a cast. Do not Copy field by field into a real platform array, which is what Capy does internally. |
typedef struct _WSABUF {
ULONG len; // Length (note: first!)
CHAR* buf; // Pointer
} WSABUF;
Windows uses WSABUF with WSARecv() and WSASend(). The field order is reversed and the length is 32-bit, so Capy copies descriptors into a WSABUF array rather than casting.
Translation Process
When you call an I/O function with a buffer sequence:
template<capy::ConstBufferSequence Buffers>
capy::io_task<std::size_t> write_some(Buffers buffers);
Capy counts the buffers, fills an array of platform structures with the descriptors, calls the OS function, and returns the result.
Conversion always happens on the stack; the implementation never allocates. A fixed on-frame window of 16 descriptors is filled from the sequence and passed to the OS call. If the sequence holds more buffers than fit, the window is refilled and the call repeated:
template<capy::ConstBufferSequence Buffers>
auto platform_write(Buffers const& buffers)
{
iovec iovecs[16]; // fixed on-frame window, never heap-allocated
auto it = begin(buffers);
auto last = end(buffers);
while (it != last)
{
std::size_t count = fill_iovecs(iovecs, it, last, 16); // up to 16
auto result = writev(fd, iovecs, count);
// ... advance the window past the buffers just written
}
}
The window size is implementation-defined.
Why Vectored I/O
Consider sending an HTTP message whose headers and body sit in separate buffers. With a single-buffer API you have two options, and each costs something:
-
Copy. Allocate a buffer large enough for both, copy the headers in, copy the body after them, then send once. That is an allocation plus two copies of data you already have.
-
Call twice. Send the headers, then send the body. No copy, but two system calls rather than one. On a datagram socket it also changes the result: two datagrams instead of one.
Vectored I/O avoids both. One call transfers several non-contiguous buffers as a single logical operation:
write(fd, header, header_len); // syscall 1
write(fd, body, body_len); // syscall 2
iovec iov[2] = {{header, header_len}, {body, body_len}};
writev(fd, iov, 2); // single syscall
The data is never copied into a contiguous staging buffer; the OS reads directly from each region. The write is also atomic at the file offset level, so other processes see all of the data or none of it.
Registered Buffers
Some platforms allow buffers to be pre-registered with the kernel, removing per-operation address translation. On Linux 5.1+, io_uring supports this:
// Registration (done once)
io_uring_register_buffers(ring, buffers, count);
// Use (fast path - no translation)
io_uring_prep_write_fixed(sqe, fd, buf, len, offset, buf_index);
Windows IOCP offers a comparable optimization with pre-registered memory regions.
Corosio does not currently expose either. Every operation goes through the per-call translation described above.
Writing Efficient Code
Fewer buffers means less translation overhead:
// Prefer: single buffer when possible
auto buf = assemble_message(); // Build in one buffer
co_await capy::write(stream, buf);
// Avoid: many tiny buffers
std::array<capy::const_buffer, 100> tiny_bufs;
co_await capy::write(stream, tiny_bufs); // 100-element translation
For repeated I/O with the same structure, consider caching the platform array:
// Build once, use many times
struct message_buffers
{
std::array<iovec, 3> iovecs;
void set_header(void const* p, std::size_t n);
void set_body(void const* p, std::size_t n);
void set_footer(void const* p, std::size_t n);
};
Buffer translation is rarely the bottleneck. Profile network latency, disk time, and your own processing before optimizing descriptor copying.