File I/O
Corosio provides two classes for asynchronous file operations:
stream_file for sequential access and random_access_file for
offset-based access. On the epoll, kqueue, and select I/O backends, both
dispatch I/O to a worker thread. The io_uring I/O backend submits it directly,
and Windows uses native overlapped I/O.
|
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 stream_file::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 |
|
Write at the end of the file ( |
|
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 stream_file::seek for repositioning.
Native Handle Access
Both file types support releasing and adopting native handles.
stream_file::release and random_access_file::release transfer
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());
stream_file::assign and random_access_file::assign adopt 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_stream_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 epoll,
kqueue, and select I/O 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_stream_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. Of the synchronous operations, only size() and release()
throw std::system_error. Both throw on a closed file, and size() also
throws when the size query fails. On Windows, release() also throws on an
open file in the cases the note above describes.
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.