File I/O
Corosio provides two classes for asynchronous file operations:
stream_file for sequential access and random_access_file for
offset-based access. Both dispatch I/O to a worker thread on POSIX
platforms and use native overlapped I/O on Windows.
|
Code snippets assume:
|
Stream File
stream_file reads and writes sequentially, maintaining an internal
position that advances after each operation. It inherits from io_stream,
so it works with any io_stream algorithm.
Reading a File
corosio::stream_file f(ioc);
if (auto ec = f.open("data.bin", corosio::file_base::read_only))
co_return; // open failed
char buf[4096];
auto [ec, n] = co_await f.read_some(capy::mutable_buffer(buf, sizeof(buf)));
if (ec == capy::cond::eof)
{
// reached end of file
eof_seen = true;
co_return;
}
Writing a File
corosio::stream_file f(ioc);
if (auto ec = f.open(
"output.bin",
corosio::file_base::write_only | corosio::file_base::create |
corosio::file_base::truncate))
co_return; // open failed
std::string data = "hello world";
auto [ec, n] =
co_await f.write_some(capy::const_buffer(data.data(), data.size()));
Seeking
The file position can be moved with seek():
auto [ec, pos] = f.seek(0, corosio::file_base::seek_set); // beginning
if (!ec)
std::tie(ec, pos) =
f.seek(100, corosio::file_base::seek_cur); // forward 100 bytes
if (!ec)
std::tie(ec, pos) =
f.seek(-10, corosio::file_base::seek_end); // 10 before end
Random Access File
random_access_file reads and writes at explicit byte offsets
without maintaining an internal position. This is useful for
databases, indices, or any workload that accesses non-sequential
regions of a file.
Open Flags
Both file types accept a bitmask of file_base::flags when opening:
| Flag | Meaning |
|---|---|
|
Open for reading (default) |
|
Open for writing |
|
Open for both reading and writing |
|
Create the file if it does not exist |
|
Fail if the file already exists (requires |
|
Truncate the file to zero length on open |
|
Seek to end on open ( |
|
Synchronize data to disk on each write |
Flags are combined with |:
if (auto ec = f.open(
"log.txt",
corosio::file_base::write_only | corosio::file_base::create |
corosio::file_base::append))
return; // report the error
File Metadata
Both file types provide synchronous metadata operations:
auto bytes = f.size(); // file size in bytes
if (auto ec = f.resize(1024)) // truncate or extend
return;
if (auto ec = f.sync_data()) // flush data to stable storage
return;
if (auto ec = f.sync_all()) // flush data and metadata
return;
stream_file additionally provides seek() for repositioning.
Native Handle Access
Both file types support releasing and adopting native handles.
release() transfers ownership out of the file object. The caller
becomes responsible for closing the handle:
// Release ownership — caller must close the handle
auto handle = f.release();
assert(!f.is_open());
assign() adopts a handle obtained from the platform’s file API, such
as open() or CreateFile:
// Adopt a handle obtained from the platform's file API —
// the file object takes ownership
corosio::random_access_file f2(ioc);
auto ec = f2.assign(native_handle);
|
On Windows, |
On POSIX, assign() accepts only what a file object can position:
regular files, block devices, and character devices. A pipe or socket is
rejected with errc::operation_not_supported — adopt those into a
posix_descriptor instead; a
directory is not adoptable by either type.
A character device that cannot seek, such as a tty, passes adoption on
every backend. What happens next is backend-specific: the POSIX
backends issue preadv/pwritev and fail at the first read or write
with ESPIPE. io_uring submits READV/WRITEV at offset -1 for a
stream_file and reads the tty successfully. Adopt one into a
posix_descriptor if you want
the same behavior everywhere.
Error Handling
File operations follow the same error model as sockets. Reads past
end-of-file return capy::cond::eof:
auto [ec, n] = co_await f.read_some(buf);
if (ec == capy::cond::eof)
{
// no more data
}
else if (ec)
{
// I/O error
}
Synchronous operations that can fail in normal use return a
std::error_code. Those are open, resize, sync_data, sync_all,
and assign. seek returns the code together with the new position.
Opening a nonexistent file with read_only reports
no_such_file_or_directory. Use create to create files that may
not exist. Only misuse, such as calling size() or release() on
a closed file, throws std::system_error.
Thread Safety
-
Distinct objects are safe to use concurrently.
-
random_access_filesupports multiple concurrent reads and writes from coroutines sharing the same file object. Each operation is independently heap-allocated. -
stream_fileallows at most one asynchronous operation in flight at a time (same as Asio’s stream_file). Sequential access with an implicit position makes concurrent ops semantically undefined. -
Non-async operations (open, close, size, resize, etc.) require external synchronization.
Platform Notes
On Linux, macOS, and BSD, the default epoll/kqueue backend dispatches
file I/O to a shared worker thread pool using preadv/pwritev. This
is the same pool used by the resolver. The io_uring backend submits
reads and writes directly instead.
On Windows, file I/O uses native IOCP overlapped I/O via
ReadFile/WriteFile with FILE_FLAG_OVERLAPPED.