Skip to content

Files

coio provides two file wrappers over the I/O object model: stream_file, which maintains an internal file position that advances with each read or write, and random_access_file, which has no internal position — every operation names an explicit offset. Both offer synchronous and asynchronous (sender-returning) operations.

Header: #include <coio/asyncio/file.h>

Overview

Type Access pattern Data-path members
stream_file<IoScheduler> Sequential; internal position read_some / write_some / async_read_some / async_write_some, plus seek
random_access_file<IoScheduler> Positional; caller-supplied offset read_some_at / write_some_at / async_read_some_at / async_write_some_at

Both share a common base surface (open, close, cancel, is_open, native_handle, get_io_scheduler, resize, size, sync_all, sync_data). The native handle type is int on Linux and HANDLE (void*) on Windows.

Platform support

Regular files are supported on uring_context (Linux) and iocp_context (Windows). They are not supported on epoll_context: epoll(7) cannot wait on regular files, so epoll_context::scheduler::make_io_object throws std::system_error with std::errc::operation_not_permitted when handed a regular file or directory descriptor — i.e. open()ing (or constructing with a path) a stream_file/random_access_file on an epoll_context scheduler throws. Default-constructing the closed wrapper is fine.

Synopsis

namespace coio {
    template<io_scheduler IoScheduler>
    class stream_file /* : file-base */ {
    public:
        using native_handle_type = /* int on Linux, void* (HANDLE) on Windows */;
        using scheduler_type = IoScheduler;
        using enum /* open_mode */;     // read_only, write_only, read_write, append,
                                        // create, exclusive, truncate, sync_all_on_write
        using enum /* seek_whence */;   // seek_set, seek_cur, seek_end

        explicit stream_file(scheduler_type scheduler) noexcept;
        stream_file(scheduler_type scheduler, native_handle_type handle);
        stream_file(IoScheduler scheduler, zstring_view path, /* open_mode */ mode);
        stream_file(stream_file&&) = default;                 // move-only

        auto open(zstring_view path, /* open_mode */ mode) -> void;
        auto close() -> void;
        auto cancel() -> void;
        auto is_open() const noexcept -> bool;
        explicit operator bool() const noexcept;
        auto native_handle() const noexcept -> native_handle_type;
        auto get_io_scheduler() const noexcept -> scheduler_type;

        auto size() const -> std::size_t;
        auto resize(std::size_t new_size) -> void;
        auto seek(std::size_t offset, /* seek_whence */ whence) -> std::size_t;
        auto sync_all() -> void;
        auto sync_data() -> void;

        auto read_some(std::span<std::byte> buffer) -> std::size_t;
        auto write_some(std::span<const std::byte> buffer) -> std::size_t;
        auto async_read_some(std::span<std::byte> buffer);        // sender of std::size_t
        auto async_write_some(std::span<const std::byte> buffer); // sender of std::size_t
    };

    template<io_scheduler IoScheduler>
    class random_access_file /* : file-base */ {
    public:
        // same constructors, open/close/cancel/is_open/native_handle/get_io_scheduler,
        // size/resize/sync_all/sync_data as stream_file, but no seek; data path:
        auto read_some_at(std::size_t offset, std::span<std::byte> buffer) -> std::size_t;
        auto write_some_at(std::size_t offset, std::span<const std::byte> buffer) -> std::size_t;
        auto async_read_some_at(std::size_t offset, std::span<std::byte> buffer);        // sender of std::size_t
        auto async_write_some_at(std::size_t offset, std::span<const std::byte> buffer); // sender of std::size_t
    };
}

API Reference

Open modes

The open mode is a flag enum; combine flags with |. The enumerators are re-exported into both file classes (stream_file::read_only, random_access_file::create, ...).

Flag Meaning
read_only Open for reading only
write_only Open for writing only
read_write Open for both reading and writing
append Writes occur at end of file
create Create the file if it doesn't exist
exclusive Ensure creation of a new file (fails if the file exists)
truncate Truncate an existing file to zero length
sync_all_on_write Synchronize all writes to disk immediately

Combining flags

Not every combination is meaningful. The rules:

Group Flags Rule
Access — pick exactly one read_only, write_only, read_write Exactly one access flag must be present. Combining two is invalid and platform-divergent: POSIX O_RDONLY is 0, so e.g. read_only \| write_only silently degenerates to write-only on Linux while granting full read-write on Windows.
Position append Requires write access (write_only or read_write).
Creation create Creates the file if missing, opens the existing file otherwise.
exclusive Only valid together with create: the open fails if the file already exists. Without create, behavior is undefined (POSIX O_EXCL semantics).
truncate Requires write access. With create: create-or-truncate. Without create: the file must already exist, or the open fails.
Durability sync_all_on_write Freely combinable; meaningful only with write access.

Common recipes (matching fopen modes):

Intent Combination
Read an existing file ("r") read_only
Overwrite or create ("w") write_only \| create \| truncate
Append or create ("a") write_only \| create \| append
Create a brand-new file, fail if it exists ("wx") write_only \| create \| exclusive
Read and write an existing file ("r+") read_write
Read and write, create-or-truncate ("w+") read_write \| create \| truncate

Common members (both classes)

explicit stream_file(scheduler_type scheduler) noexcept

Constructs a closed file bound to scheduler.

stream_file(scheduler_type scheduler, native_handle_type handle)

Adopts an already-open native handle. The file object takes ownership. On Windows the handle must have been opened with FILE_FLAG_OVERLAPPED.

stream_file(IoScheduler scheduler, zstring_view path, mode)

Constructs and opens; equivalent to default construction followed by open(path, mode). Throws std::system_error on failure.

open(zstring_view path, mode) -> void

Opens the file at path. Throws std::system_error with coio::error::already_open if the file is already open, or with an OS error code on failure (including the epoll_context regular-file rejection described above).

close() -> void

Releases the handle and resets the object to the not-open state. Precondition: no outstanding asynchronous operations — every operation must have completed before the call (cancel() first and await the completions if needed). Throws std::system_error on OS failure. The destructor closes implicitly, under the same precondition. See close semantics per backend.

cancel() -> void

Cancels outstanding asynchronous read/write operations on this file; they complete with set_stopped().

is_open() const noexcept -> bool / explicit operator bool()

Whether the file holds a valid handle.

native_handle() const noexcept / get_io_scheduler() const noexcept

Access the underlying handle / the bound scheduler.

size() const -> std::size_t

Current file size in bytes. Throws std::system_error on failure.

resize(std::size_t new_size) -> void

Sets the file size (truncates or extends). Throws std::system_error on failure.

sync_all() -> void / sync_data() -> void

Block until modified data (and, for sync_all, metadata) reaches the storage device. Throw std::system_error on failure.

stream_file data path

read_some(std::span<std::byte> buffer) -> std::size_t

Synchronously reads up to buffer.size() bytes at the current position and advances the position by the amount read. Returns the number of bytes read (at least 1 for a non-empty buffer). Throws std::system_error; end-of-file is reported as coio::error::eof. An empty buffer returns 0 immediately.

write_some(std::span<const std::byte> buffer) -> std::size_t

Synchronously writes up to buffer.size() bytes at the current position and advances the position. Returns the number of bytes written. Throws std::system_error on failure.

async_read_some(std::span<std::byte> buffer)

Returns a sender that reads up to buffer.size() bytes at the current stream position and advances it. Completes with set_value(std::size_t), or set_error(std::error_code) — end-of-file arrives as coio::error::eof — or set_stopped() on cancellation.

async_write_some(std::span<const std::byte> buffer)

Returns a sender that writes up to buffer.size() bytes at the current stream position and advances it. Completes with set_value(std::size_t) / set_error(std::error_code) / set_stopped().

seek(std::size_t offset, whence) -> std::size_t

Moves the stream position; returns the new absolute position. whence is one of seek_set (from beginning), seek_cur (from current), seek_end (from end). Throws std::system_error on failure.

IOCP: the stream offset is consumed when the sender is built

On iocp_context the stream position lives in the io object, and async_read_some/async_write_some reserve their offset range at sender-creation time (Asio-style). A sender that is created but never started still advances the position. Create-then-start senders one at a time, in order (which the outstanding-operation limits require anyway).

random_access_file data path

random_access_file has no position and no seek; the buffer must stay valid until completion, and concurrent operations at different offsets are still subject to the one-read + one-write outstanding limit.

read_some_at(std::size_t offset, std::span<std::byte> buffer) -> std::size_t

Synchronous positional read; does not modify any file position. EOF throws coio::error::eof.

write_some_at(std::size_t offset, std::span<const std::byte> buffer) -> std::size_t

Synchronous positional write; does not modify any file position.

async_read_some_at(std::size_t offset, std::span<std::byte> buffer)

Sender of std::size_t; completes with coio::error::eof via set_error if the read hits end-of-file (0 bytes into a non-empty buffer).

async_write_some_at(std::size_t offset, std::span<const std::byte> buffer)

Sender of std::size_t.

Thread safety

File objects are not thread-safe: do not call member functions concurrently on the same object. The outstanding-operation limits and lifetime rules of the I/O object model apply.

Example

Positional file copy with random_access_file (adapted from examples/cp.cpp):

#include <coio/core.h>
#include <coio/asyncio/io.h>
#include <coio/asyncio/file.h>

#if COIO_OS_LINUX
#include <coio/asyncio/uring_context.h>     // epoll_context does not support files
using io_context = coio::uring_context;
#elif COIO_OS_WINDOWS
#include <coio/asyncio/iocp_context.h>
using io_context = coio::iocp_context;
#endif

using file = coio::random_access_file<io_context::scheduler>;

auto copy_file(coio::zstring_view src, coio::zstring_view dst) -> io_context::task<> {
    auto sched = co_await coio::read_scheduler();
    file src_file{sched, src, file::read_only};
    file dst_file{sched, dst, file::write_only | file::create | file::truncate};

    const std::size_t total = src_file.size();
    dst_file.resize(total);

    try {
        std::byte buffer[1024];
        std::size_t offset = 0;
        while (offset < total) {
            const auto n = co_await src_file.async_read_some_at(
                offset, coio::as_writable_bytes(buffer));
            co_await coio::async_write_at(dst_file, offset, coio::as_bytes(buffer, n));
            offset += n;
        }
    }
    catch (const std::system_error& e) {
        if (e.code() != coio::error::eof) throw;
    }
    dst_file.sync_all();
}

auto main(int argc, char** argv) -> int {
    if (argc != 3) return 1;
    io_context context;
    coio::this_thread::sync_wait(coio::when_all(
        coio::starts_on(context.get_scheduler(), copy_file(argv[1], argv[2])),
        [&]() -> coio::task<> { context.run(); co_return; }()
    ));
}

See also