diff --git a/doc/modules/ROOT/nav.adoc b/doc/modules/ROOT/nav.adoc index 20e7905f9..a32ba9bed 100644 --- a/doc/modules/ROOT/nav.adoc +++ b/doc/modules/ROOT/nav.adoc @@ -34,6 +34,7 @@ ** xref:4.guide/4l.tls.adoc[TLS Encryption] ** xref:4.guide/4m.error-handling.adoc[Error Handling] ** xref:4.guide/4n.buffers.adoc[Buffer Sequences] +** xref:4.guide/4o.file-io.adoc[File I/O] * xref:5.testing/5.intro.adoc[Testing] ** xref:5.testing/5a.mocket.adoc[Mock Sockets] * xref:benchmark-report.adoc[Benchmarks] diff --git a/doc/modules/ROOT/pages/4.guide/4o.file-io.adoc b/doc/modules/ROOT/pages/4.guide/4o.file-io.adoc new file mode 100644 index 000000000..eaa4bf22b --- /dev/null +++ b/doc/modules/ROOT/pages/4.guide/4o.file-io.adoc @@ -0,0 +1,203 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + += 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. + +[NOTE] +==== +Code snippets assume: +[source,cpp] +---- +#include +#include +namespace corosio = boost::corosio; +---- +==== + +== 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 algorithm that accepts an `io_stream&`. + +=== Reading a File + +[source,cpp] +---- +corosio::stream_file f(ioc); +f.open("data.bin", corosio::file_base::read_only); + +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 +---- + +=== Writing a File + +[source,cpp] +---- +corosio::stream_file f(ioc); +f.open("output.bin", + corosio::file_base::write_only + | corosio::file_base::create + | corosio::file_base::truncate); + +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()`: + +[source,cpp] +---- +f.seek(0, corosio::file_base::seek_set); // beginning +f.seek(100, corosio::file_base::seek_cur); // forward 100 bytes +f.seek(-10, corosio::file_base::seek_end); // 10 bytes 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. + +=== Reading at an Offset + +[source,cpp] +---- +corosio::random_access_file f(ioc); +f.open("data.bin", corosio::file_base::read_only); + +char buf[256]; +auto [ec, n] = co_await f.read_some_at( + 1024, // byte offset + capy::mutable_buffer(buf, sizeof(buf))); +---- + +=== Writing at an Offset + +[source,cpp] +---- +corosio::random_access_file f(ioc); +f.open("data.bin", corosio::file_base::read_write); + +auto [ec, n] = co_await f.write_some_at( + 512, capy::const_buffer("patched", 7)); +---- + +== Open Flags + +Both file types accept a bitmask of `file_base::flags` when opening: + +[cols="1,3"] +|=== +| Flag | Meaning + +| `read_only` | Open for reading (default) +| `write_only` | Open for writing +| `read_write` | Open for both reading and writing +| `create` | Create the file if it does not exist +| `exclusive` | Fail if the file already exists (requires `create`) +| `truncate` | Truncate the file to zero length on open +| `append` | Seek to end on open (stream_file only) +| `sync_all_on_write` | Synchronize data to disk on each write +|=== + +Flags are combined with `|`: + +[source,cpp] +---- +f.open("log.txt", + corosio::file_base::write_only + | corosio::file_base::create + | corosio::file_base::append); +---- + +== File Metadata + +Both file types provide synchronous metadata operations: + +[source,cpp] +---- +auto bytes = f.size(); // file size in bytes +f.resize(1024); // truncate or extend +f.sync_data(); // flush data to stable storage +f.sync_all(); // flush data and metadata +---- + +`stream_file` additionally provides `seek()` for repositioning. + +== Native Handle Access + +Both file types support adopting and releasing native handles: + +[source,cpp] +---- +// Release ownership — caller must close the handle +auto handle = f.release(); +assert(!f.is_open()); + +// Adopt an existing handle — file takes ownership +corosio::random_access_file f2(ioc); +f2.assign(handle); +---- + +== Error Handling + +File operations follow the same error model as sockets. Reads past +end-of-file return `capy::cond::eof`: + +[source,cpp] +---- +auto [ec, n] = co_await f.read_some(buf); +if (ec == capy::cond::eof) +{ + // no more data +} +else if (ec) +{ + // I/O error +} +---- + +Opening a nonexistent file with `read_only` throws `std::system_error`. +Use `create` to create files that may not exist. + +== Thread Safety + +* Distinct objects are safe to use concurrently. +* `random_access_file` supports multiple concurrent reads and writes + from coroutines sharing the same file object. Each operation is + independently heap-allocated. +* `stream_file` allows 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, file I/O is dispatched to a shared worker +thread pool using `preadv`/`pwritev`. This is the same pool used by +the resolver. + +On Windows, file I/O uses native IOCP overlapped I/O via +`ReadFile`/`WriteFile` with `FILE_FLAG_OVERLAPPED`. diff --git a/include/boost/corosio.hpp b/include/boost/corosio.hpp index 8ff521b8a..e0b2aacd6 100644 --- a/include/boost/corosio.hpp +++ b/include/boost/corosio.hpp @@ -13,13 +13,16 @@ #include #include #include +#include #include #include #include +#include #include #include #include #include +#include #include #include #include diff --git a/include/boost/corosio/backend.hpp b/include/boost/corosio/backend.hpp index 6717f8f60..045a843f8 100644 --- a/include/boost/corosio/backend.hpp +++ b/include/boost/corosio/backend.hpp @@ -176,6 +176,11 @@ class win_signals; class win_resolver; class win_resolver_service; +class win_stream_file; +class win_file_service; +class win_random_access_file; +class win_random_access_file_service; + } // namespace detail /// Backend tag for the Windows I/O Completion Ports multiplexer. diff --git a/include/boost/corosio/detail/file_service.hpp b/include/boost/corosio/detail/file_service.hpp new file mode 100644 index 000000000..9de447ff7 --- /dev/null +++ b/include/boost/corosio/detail/file_service.hpp @@ -0,0 +1,60 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_DETAIL_FILE_SERVICE_HPP +#define BOOST_COROSIO_DETAIL_FILE_SERVICE_HPP + +#include +#include +#include + +#include +#include + +namespace boost::corosio::detail { + +/** Abstract stream file service base class. + + Concrete implementations (posix, IOCP) inherit from + this class and provide platform-specific file operations. + The context constructor installs whichever backend via + `make_service`, and `stream_file.cpp` retrieves it via + `use_service()`. +*/ +class BOOST_COROSIO_DECL file_service + : public capy::execution_context::service + , public io_object::io_service +{ +public: + /// Identifies this service for `execution_context` lookup. + using key_type = file_service; + + /** Open a file. + + Opens the file at the given path with the specified flags + and associates it with the platform I/O mechanism. + + @param impl The file implementation to initialize. + @param path The filesystem path to open. + @param mode Bitmask of file_base::flags. + @return Error code on failure, empty on success. + */ + virtual std::error_code open_file( + stream_file::implementation& impl, + std::filesystem::path const& path, + file_base::flags mode) = 0; + +protected: + file_service() = default; + ~file_service() override = default; +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_DETAIL_FILE_SERVICE_HPP diff --git a/include/boost/corosio/detail/intrusive.hpp b/include/boost/corosio/detail/intrusive.hpp index 51605feab..5211203ee 100644 --- a/include/boost/corosio/detail/intrusive.hpp +++ b/include/boost/corosio/detail/intrusive.hpp @@ -65,10 +65,11 @@ class intrusive_list void push_back(T* w) noexcept { - w->next_ = nullptr; - w->prev_ = tail_; + auto* n = static_cast(w); + n->next_ = nullptr; + n->prev_ = tail_; if (tail_) - tail_->next_ = w; + static_cast(tail_)->next_ = w; else head_ = w; tail_ = w; @@ -80,9 +81,9 @@ class intrusive_list return; if (tail_) { - tail_->next_ = other.head_; - other.head_->prev_ = tail_; - tail_ = other.tail_; + static_cast(tail_)->next_ = other.head_; + static_cast(other.head_)->prev_ = tail_; + tail_ = other.tail_; } else { @@ -98,28 +99,43 @@ class intrusive_list if (!head_) return nullptr; T* w = head_; - head_ = head_->next_; + head_ = static_cast(head_)->next_; if (head_) - head_->prev_ = nullptr; + static_cast(head_)->prev_ = nullptr; else tail_ = nullptr; // Defensive: clear stale linkage so remove() on a // popped node cannot corrupt the list. - w->next_ = nullptr; - w->prev_ = nullptr; + auto* n = static_cast(w); + n->next_ = nullptr; + n->prev_ = nullptr; return w; } void remove(T* w) noexcept { - if (w->prev_) - w->prev_->next_ = w->next_; + auto* n = static_cast(w); + // Already detached — nothing to do. + if (!n->next_ && !n->prev_ && head_ != w && tail_ != w) + return; + if (n->prev_) + static_cast(n->prev_)->next_ = n->next_; else - head_ = w->next_; - if (w->next_) - w->next_->prev_ = w->prev_; + head_ = n->next_; + if (n->next_) + static_cast(n->next_)->prev_ = n->prev_; else - tail_ = w->prev_; + tail_ = n->prev_; + n->next_ = nullptr; + n->prev_ = nullptr; + } + + /// Invoke @p f for each element in the list. + template + void for_each(F f) + { + for (T* p = head_; p; p = static_cast(p)->next_) + f(p); } }; diff --git a/include/boost/corosio/detail/platform.hpp b/include/boost/corosio/detail/platform.hpp index cc0ae48de..2a128a1b6 100644 --- a/include/boost/corosio/detail/platform.hpp +++ b/include/boost/corosio/detail/platform.hpp @@ -64,6 +64,16 @@ #define BOOST_COROSIO_POSIX 0 #endif +// fdatasync availability — Linux and FreeBSD provide it. +// macOS does not have fdatasync; sync_data() falls back to fsync(). +// We use platform detection rather than _POSIX_SYNCHRONIZED_IO +// because may not have been included yet. +#if defined(__linux__) || defined(__FreeBSD__) +#define BOOST_COROSIO_HAS_POSIX_SYNCHRONIZED_IO 1 +#else +#define BOOST_COROSIO_HAS_POSIX_SYNCHRONIZED_IO 0 +#endif + #endif // BOOST_COROSIO_MRDOCS #endif // BOOST_COROSIO_DETAIL_PLATFORM_HPP diff --git a/include/boost/corosio/detail/random_access_file_service.hpp b/include/boost/corosio/detail/random_access_file_service.hpp new file mode 100644 index 000000000..32a6bd164 --- /dev/null +++ b/include/boost/corosio/detail/random_access_file_service.hpp @@ -0,0 +1,54 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_DETAIL_RANDOM_ACCESS_FILE_SERVICE_HPP +#define BOOST_COROSIO_DETAIL_RANDOM_ACCESS_FILE_SERVICE_HPP + +#include +#include +#include + +#include +#include + +namespace boost::corosio::detail { + +/** Abstract random-access file service base class. + + Concrete implementations (posix, IOCP) inherit from + this class and provide platform-specific file operations. +*/ +class BOOST_COROSIO_DECL random_access_file_service + : public capy::execution_context::service + , public io_object::io_service +{ +public: + /// Identifies this service for `execution_context` lookup. + using key_type = random_access_file_service; + + /** Open a file. + + @param impl The file implementation to initialize. + @param path The filesystem path to open. + @param mode Bitmask of file_base::flags. + @return Error code on failure, empty on success. + */ + virtual std::error_code open_file( + random_access_file::implementation& impl, + std::filesystem::path const& path, + file_base::flags mode) = 0; + +protected: + random_access_file_service() = default; + ~random_access_file_service() override = default; +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_DETAIL_RANDOM_ACCESS_FILE_SERVICE_HPP diff --git a/include/boost/corosio/file_base.hpp b/include/boost/corosio/file_base.hpp new file mode 100644 index 000000000..87dc0e972 --- /dev/null +++ b/include/boost/corosio/file_base.hpp @@ -0,0 +1,94 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_FILE_BASE_HPP +#define BOOST_COROSIO_FILE_BASE_HPP + +#include + +namespace boost::corosio { + +/** Common definitions for file I/O objects. + + Provides open flags and seek origin constants shared + by @ref stream_file and @ref random_access_file. +*/ +struct file_base +{ + /** Bitmask flags for opening a file. + + Flags are combined with bitwise OR to specify the + desired access mode and creation behavior. + */ + enum flags : unsigned + { + /// Open for reading only. + read_only = 1, + + /// Open for writing only. + write_only = 2, + + /// Open for reading and writing. + read_write = read_only | write_only, + + /// Append to the end of the file on each write. + append = 4, + + /// Create the file if it does not exist. + create = 8, + + /// Fail if the file already exists (requires @ref create). + exclusive = 16, + + /// Truncate the file to zero length on open. + truncate = 32, + + /// Synchronize data to disk on each write. + sync_all_on_write = 64 + }; + + /** Origin for seek operations. */ + enum seek_basis + { + /// Seek relative to the beginning of the file. + seek_set, + + /// Seek relative to the current position. + seek_cur, + + /// Seek relative to the end of the file. + seek_end + }; + + friend constexpr flags operator|(flags a, flags b) noexcept + { + return static_cast( + static_cast(a) | static_cast(b)); + } + + friend constexpr flags operator&(flags a, flags b) noexcept + { + return static_cast( + static_cast(a) & static_cast(b)); + } + + friend constexpr flags& operator|=(flags& a, flags b) noexcept + { + return a = a | b; + } + + friend constexpr flags& operator&=(flags& a, flags b) noexcept + { + return a = a & b; + } +}; + +} // namespace boost::corosio + +#endif // BOOST_COROSIO_FILE_BASE_HPP diff --git a/include/boost/corosio/native/detail/epoll/epoll_scheduler.hpp b/include/boost/corosio/native/detail/epoll/epoll_scheduler.hpp index c17ad5951..2efe82a0c 100644 --- a/include/boost/corosio/native/detail/epoll/epoll_scheduler.hpp +++ b/include/boost/corosio/native/detail/epoll/epoll_scheduler.hpp @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include @@ -190,6 +192,8 @@ inline epoll_scheduler::epoll_scheduler(capy::execution_context& ctx, int) get_resolver_service(ctx, *this); get_signal_service(ctx, *this); + get_stream_file_service(ctx, *this); + get_random_access_file_service(ctx, *this); completed_ops_.push(&task_op_); } diff --git a/include/boost/corosio/native/detail/iocp/win_file_service.hpp b/include/boost/corosio/native/detail/iocp/win_file_service.hpp new file mode 100644 index 000000000..9a001f27b --- /dev/null +++ b/include/boost/corosio/native/detail/iocp/win_file_service.hpp @@ -0,0 +1,817 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_FILE_SERVICE_HPP +#define BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_FILE_SERVICE_HPP + +#include + +#if BOOST_COROSIO_HAS_IOCP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace boost::corosio::detail { + +/** Windows IOCP stream file management service. + + Owns all stream file implementations and coordinates their + lifecycle with the IOCP. + + @par Thread Safety + All public member functions are thread-safe. +*/ +class BOOST_COROSIO_DECL win_file_service final + : public file_service +{ +public: + using key_type = win_file_service; + + explicit win_file_service(capy::execution_context& ctx); + ~win_file_service(); + + win_file_service(win_file_service const&) = delete; + win_file_service& operator=(win_file_service const&) = delete; + + io_object::implementation* construct() override; + void destroy(io_object::implementation* p) override; + void close(io_object::handle& h) override; + void shutdown() override; + + std::error_code open_file( + stream_file::implementation& impl, + std::filesystem::path const& path, + file_base::flags mode) override; + + void destroy_impl(win_stream_file& impl); + void unregister_impl(win_stream_file_internal& impl); + + void post(overlapped_op* op); + void on_pending(overlapped_op* op) noexcept; + void on_completion(overlapped_op* op, DWORD error, DWORD bytes) noexcept; + void work_started() noexcept; + void work_finished() noexcept; + + void* iocp_handle() const noexcept; + + /** Attempt data-only flush via NtFlushBuffersFileEx. + + @return true if data-only flush succeeded, false if + caller should fall back to FlushFileBuffers. + */ + bool try_flush_data(HANDLE h) noexcept; + +private: + // NtFlushBuffersFileEx support for data-only sync + struct io_status_block + { + union { LONG Status; void* Pointer; }; + ULONG_PTR Information; + }; + + enum { flush_flags_file_data_sync_only = 4 }; + + using nt_flush_fn = LONG(NTAPI*)( + HANDLE, ULONG, void*, ULONG, io_status_block*); + + win_scheduler& sched_; + win_mutex mutex_; + intrusive_list file_list_; + intrusive_list wrapper_list_; + void* iocp_; + nt_flush_fn nt_flush_buffers_file_ex_; +}; + +/** Get or create the stream file service for the given context. */ +inline win_file_service& +get_stream_file_service(capy::execution_context& ctx, win_scheduler&) +{ + return ctx.make_service(); +} + +// --------------------------------------------------------------------------- +// Operation constructors +// --------------------------------------------------------------------------- + +inline file_read_op::file_read_op(win_stream_file_internal& f) noexcept + : overlapped_op(&do_complete) + , file_(f) +{ + cancel_func_ = &do_cancel_impl; +} + +inline file_write_op::file_write_op(win_stream_file_internal& f) noexcept + : overlapped_op(&do_complete) + , file_(f) +{ + cancel_func_ = &do_cancel_impl; +} + +// --------------------------------------------------------------------------- +// Cancellation functions +// --------------------------------------------------------------------------- + +inline void +file_read_op::do_cancel_impl(overlapped_op* base) noexcept +{ + auto* op = static_cast(base); + op->cancelled.store(true, std::memory_order_release); + if (op->file_.is_open()) + ::CancelIoEx(op->file_.native_handle(), op); +} + +inline void +file_write_op::do_cancel_impl(overlapped_op* base) noexcept +{ + auto* op = static_cast(base); + op->cancelled.store(true, std::memory_order_release); + if (op->file_.is_open()) + ::CancelIoEx(op->file_.native_handle(), op); +} + +// --------------------------------------------------------------------------- +// Completion handlers +// --------------------------------------------------------------------------- + +inline void +file_read_op::do_complete( + void* owner, + scheduler_op* base, + std::uint32_t /*bytes*/, + std::uint32_t /*error*/) +{ + auto* op = static_cast(base); + + if (!owner) + { + op->cleanup_only(); + op->file_ptr.reset(); + return; + } + + // Advance stream position on success + if (op->dwError == 0 && op->bytes_transferred > 0) + op->file_.offset_ += op->bytes_transferred; + + auto prevent_premature_destruction = std::move(op->file_ptr); + op->invoke_handler(); +} + +inline void +file_write_op::do_complete( + void* owner, + scheduler_op* base, + std::uint32_t /*bytes*/, + std::uint32_t /*error*/) +{ + auto* op = static_cast(base); + + if (!owner) + { + op->cleanup_only(); + op->file_ptr.reset(); + return; + } + + // Advance stream position on success + if (op->dwError == 0 && op->bytes_transferred > 0) + op->file_.offset_ += op->bytes_transferred; + + auto prevent_premature_destruction = std::move(op->file_ptr); + op->invoke_handler(); +} + +// --------------------------------------------------------------------------- +// win_stream_file_internal +// --------------------------------------------------------------------------- + +inline +win_stream_file_internal::win_stream_file_internal( + win_file_service& svc) noexcept + : svc_(svc) + , rd_(*this) + , wr_(*this) +{ +} + +inline +win_stream_file_internal::~win_stream_file_internal() +{ + svc_.unregister_impl(*this); +} + +inline HANDLE +win_stream_file_internal::native_handle() const noexcept +{ + return handle_; +} + +inline bool +win_stream_file_internal::is_open() const noexcept +{ + return handle_ != INVALID_HANDLE_VALUE; +} + +inline void +win_stream_file_internal::cancel() noexcept +{ + if (handle_ != INVALID_HANDLE_VALUE) + ::CancelIoEx(handle_, nullptr); + + rd_.request_cancel(); + wr_.request_cancel(); +} + +inline void +win_stream_file_internal::close_handle() noexcept +{ + if (handle_ != INVALID_HANDLE_VALUE) + { + ::CancelIoEx(handle_, nullptr); + ::CloseHandle(handle_); + handle_ = INVALID_HANDLE_VALUE; + } + offset_ = 0; +} + +inline std::uint64_t +win_stream_file_internal::size() const +{ + LARGE_INTEGER li; + if (!::GetFileSizeEx(handle_, &li)) + throw_system_error(make_err(::GetLastError()), "stream_file::size"); + return static_cast(li.QuadPart); +} + +inline void +win_stream_file_internal::resize(std::uint64_t new_size) +{ + LARGE_INTEGER li; + li.QuadPart = static_cast(new_size); + if (!::SetFilePointerEx(handle_, li, nullptr, FILE_BEGIN)) + throw_system_error(make_err(::GetLastError()), "stream_file::resize"); + if (!::SetEndOfFile(handle_)) + throw_system_error(make_err(::GetLastError()), "stream_file::resize"); +} + +inline void +win_stream_file_internal::sync_data() +{ + // Attempt data-only flush; fall back to full flush + if (svc_.try_flush_data(handle_)) + return; + if (!::FlushFileBuffers(handle_)) + throw_system_error( + make_err(::GetLastError()), "stream_file::sync_data"); +} + +inline void +win_stream_file_internal::sync_all() +{ + if (!::FlushFileBuffers(handle_)) + throw_system_error( + make_err(::GetLastError()), "stream_file::sync_all"); +} + +inline native_handle_type +win_stream_file_internal::release() +{ + HANDLE h = handle_; + handle_ = INVALID_HANDLE_VALUE; + offset_ = 0; + return reinterpret_cast(h); +} + +inline void +win_stream_file_internal::assign(native_handle_type handle) +{ + close_handle(); + HANDLE h = reinterpret_cast(handle); + // Register with IOCP so overlapped I/O works + if (!::CreateIoCompletionPort( + h, static_cast(svc_.iocp_handle()), key_io, 0)) + { + throw_system_error( + make_err(::GetLastError()), "stream_file::assign"); + } + handle_ = h; + offset_ = 0; +} + +inline std::uint64_t +win_stream_file_internal::seek( + std::int64_t offset, file_base::seek_basis origin) +{ + // We manage offset_ ourselves (same as POSIX impl). + std::int64_t new_pos; + + if (origin == file_base::seek_set) + { + new_pos = offset; + } + else if (origin == file_base::seek_cur) + { + new_pos = static_cast(offset_) + offset; + } + else // seek_end + { + LARGE_INTEGER li; + if (!::GetFileSizeEx(handle_, &li)) + throw_system_error( + make_err(::GetLastError()), "stream_file::seek"); + new_pos = li.QuadPart + offset; + } + + if (new_pos < 0) + throw_system_error( + make_err(ERROR_NEGATIVE_SEEK), "stream_file::seek"); + + offset_ = static_cast(new_pos); + return offset_; +} + +inline std::coroutine_handle<> +win_stream_file_internal::read_some( + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param param, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) +{ + static constexpr std::size_t max_buffers = 16; + + // Keep internal alive during I/O + rd_.file_ptr = shared_from_this(); + + auto& op = rd_; + op.reset(); + op.is_read_ = true; + op.h = h; + op.ex = ex; + op.ec_out = ec; + op.bytes_out = bytes_out; + op.start(token); + + svc_.work_started(); + + // Extract first buffer from buffer_param + capy::mutable_buffer bufs[max_buffers]; + auto count = param.copy_to(bufs, max_buffers); + + if (count == 0) + { + // Empty buffer — complete with 0 bytes + op.empty_buffer = true; + svc_.on_completion(&op, 0, 0); + return std::noop_coroutine(); + } + + // ReadFile uses a single contiguous buffer + op.buf = bufs[0].data(); + op.buf_len = static_cast(bufs[0].size()); + + // Set file offset in OVERLAPPED + op.Offset = static_cast(offset_ & 0xFFFFFFFF); + op.OffsetHigh = static_cast(offset_ >> 32); + + BOOL ok = ::ReadFile(handle_, op.buf, op.buf_len, nullptr, &op); + DWORD err = ok ? 0 : ::GetLastError(); + + if (err != 0 && err != ERROR_IO_PENDING) + { + svc_.on_completion(&op, err, 0); + return std::noop_coroutine(); + } + + svc_.on_pending(&op); + + // Re-check cancellation after I/O is pending + if (op.cancelled.load(std::memory_order_acquire)) + ::CancelIoEx(handle_, &op); + + return std::noop_coroutine(); +} + +inline std::coroutine_handle<> +win_stream_file_internal::write_some( + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param param, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) +{ + static constexpr std::size_t max_buffers = 16; + + // Keep internal alive during I/O + wr_.file_ptr = shared_from_this(); + + auto& op = wr_; + op.reset(); + op.h = h; + op.ex = ex; + op.ec_out = ec; + op.bytes_out = bytes_out; + op.start(token); + + svc_.work_started(); + + // Extract first buffer from buffer_param + capy::mutable_buffer bufs[max_buffers]; + auto count = param.copy_to(bufs, max_buffers); + + if (count == 0) + { + // Empty buffer — complete with 0 bytes + svc_.on_completion(&op, 0, 0); + return std::noop_coroutine(); + } + + // WriteFile uses a single contiguous buffer + op.buf = bufs[0].data(); + op.buf_len = static_cast(bufs[0].size()); + + // Set file offset in OVERLAPPED + op.Offset = static_cast(offset_ & 0xFFFFFFFF); + op.OffsetHigh = static_cast(offset_ >> 32); + + BOOL ok = ::WriteFile(handle_, op.buf, op.buf_len, nullptr, &op); + DWORD err = ok ? 0 : ::GetLastError(); + + if (err != 0 && err != ERROR_IO_PENDING) + { + svc_.on_completion(&op, err, 0); + return std::noop_coroutine(); + } + + svc_.on_pending(&op); + + // Re-check cancellation after I/O is pending + if (op.cancelled.load(std::memory_order_acquire)) + ::CancelIoEx(handle_, &op); + + return std::noop_coroutine(); +} + +// --------------------------------------------------------------------------- +// win_stream_file wrapper +// --------------------------------------------------------------------------- + +inline +win_stream_file::win_stream_file( + std::shared_ptr internal) noexcept + : internal_(std::move(internal)) +{ +} + +inline void +win_stream_file::close_internal() noexcept +{ + if (internal_) + { + internal_->close_handle(); + internal_.reset(); + } +} + +inline std::coroutine_handle<> +win_stream_file::read_some( + std::coroutine_handle<> h, + capy::executor_ref d, + buffer_param buf, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes) +{ + return internal_->read_some(h, d, buf, token, ec, bytes); +} + +inline std::coroutine_handle<> +win_stream_file::write_some( + std::coroutine_handle<> h, + capy::executor_ref d, + buffer_param buf, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes) +{ + return internal_->write_some(h, d, buf, token, ec, bytes); +} + +inline native_handle_type +win_stream_file::native_handle() const noexcept +{ + return reinterpret_cast(internal_->native_handle()); +} + +inline void +win_stream_file::cancel() noexcept +{ + internal_->cancel(); +} + +inline std::uint64_t +win_stream_file::size() const +{ + return internal_->size(); +} + +inline void +win_stream_file::resize(std::uint64_t new_size) +{ + internal_->resize(new_size); +} + +inline void +win_stream_file::sync_data() +{ + internal_->sync_data(); +} + +inline void +win_stream_file::sync_all() +{ + internal_->sync_all(); +} + +inline native_handle_type +win_stream_file::release() +{ + return internal_->release(); +} + +inline void +win_stream_file::assign(native_handle_type handle) +{ + internal_->assign(handle); +} + +inline std::uint64_t +win_stream_file::seek(std::int64_t offset, file_base::seek_basis origin) +{ + return internal_->seek(offset, origin); +} + +inline win_stream_file_internal* +win_stream_file::get_internal() const noexcept +{ + return internal_.get(); +} + +// --------------------------------------------------------------------------- +// win_file_service +// --------------------------------------------------------------------------- + +inline +win_file_service::win_file_service(capy::execution_context& ctx) + : sched_(ctx.use_service()) + , iocp_(sched_.native_handle()) + , nt_flush_buffers_file_ex_(nullptr) +{ + if (FARPROC p = ::GetProcAddress( + ::GetModuleHandleA("NTDLL"), "NtFlushBuffersFileEx")) + { + nt_flush_buffers_file_ex_ = reinterpret_cast( + reinterpret_cast(p)); + } +} + +inline +win_file_service::~win_file_service() +{ + for (auto* w = wrapper_list_.pop_front(); w != nullptr; + w = wrapper_list_.pop_front()) + delete w; +} + +inline io_object::implementation* +win_file_service::construct() +{ + auto internal = std::make_shared(*this); + + { + std::lock_guard lock(mutex_); + file_list_.push_back(internal.get()); + } + + auto* wrapper = new win_stream_file(std::move(internal)); + + { + std::lock_guard lock(mutex_); + wrapper_list_.push_back(wrapper); + } + + return wrapper; +} + +inline void +win_file_service::destroy(io_object::implementation* p) +{ + if (p) + { + auto& wrapper = static_cast(*p); + wrapper.close_internal(); + destroy_impl(wrapper); + } +} + +inline void +win_file_service::close(io_object::handle& h) +{ + auto& wrapper = static_cast(*h.get()); + wrapper.get_internal()->close_handle(); +} + +inline void +win_file_service::shutdown() +{ + std::lock_guard lock(mutex_); + + for (auto* impl = file_list_.pop_front(); impl != nullptr; + impl = file_list_.pop_front()) + { + impl->close_handle(); + } +} + +inline std::error_code +win_file_service::open_file( + stream_file::implementation& impl, + std::filesystem::path const& path, + file_base::flags mode) +{ + // Build access mask + DWORD access = 0; + unsigned a = static_cast(mode) & 3u; + if (a == 3) + access = GENERIC_READ | GENERIC_WRITE; + else if (a == 2) + access = GENERIC_WRITE; + else + access = GENERIC_READ; + + // Build creation disposition + DWORD disposition = OPEN_EXISTING; + if ((mode & file_base::create) && (mode & file_base::exclusive)) + disposition = CREATE_NEW; + else if ((mode & file_base::create) && (mode & file_base::truncate)) + disposition = OPEN_ALWAYS; + else if (mode & file_base::create) + disposition = OPEN_ALWAYS; + else if (mode & file_base::truncate) + disposition = TRUNCATE_EXISTING; + + // Build flags — FILE_FLAG_OVERLAPPED is required for IOCP + DWORD flags = FILE_ATTRIBUTE_NORMAL + | FILE_FLAG_OVERLAPPED + | FILE_FLAG_SEQUENTIAL_SCAN; + if (mode & file_base::sync_all_on_write) + flags |= FILE_FLAG_WRITE_THROUGH; + + HANDLE h = ::CreateFileW( + path.c_str(), + access, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + disposition, + flags, + nullptr); + + if (h == INVALID_HANDLE_VALUE) + return make_err(::GetLastError()); + + // Register with IOCP + if (!::CreateIoCompletionPort( + h, static_cast(iocp_), key_io, 0)) + { + DWORD err = ::GetLastError(); + ::CloseHandle(h); + return make_err(err); + } + + // Handle truncation for create|truncate combo + if ((mode & file_base::create) && (mode & file_base::truncate) + && disposition == OPEN_ALWAYS) + { + if (!::SetEndOfFile(h)) + { + DWORD err = ::GetLastError(); + ::CloseHandle(h); + return make_err(err); + } + } + + auto& internal = *static_cast(impl).get_internal(); + internal.handle_ = h; + internal.offset_ = 0; + + // Handle append: seek to end + if (mode & file_base::append) + { + LARGE_INTEGER sz; + if (!::GetFileSizeEx(h, &sz)) + { + DWORD err = ::GetLastError(); + internal.handle_ = INVALID_HANDLE_VALUE; + ::CloseHandle(h); + return make_err(err); + } + internal.offset_ = static_cast(sz.QuadPart); + } + + return {}; +} + +inline void +win_file_service::destroy_impl(win_stream_file& impl) +{ + { + std::lock_guard lock(mutex_); + wrapper_list_.remove(&impl); + } + delete &impl; +} + +inline void +win_file_service::unregister_impl(win_stream_file_internal& impl) +{ + std::lock_guard lock(mutex_); + file_list_.remove(&impl); +} + +inline void +win_file_service::post(overlapped_op* op) +{ + sched_.post(op); +} + +inline void +win_file_service::on_pending(overlapped_op* op) noexcept +{ + sched_.on_pending(op); +} + +inline void +win_file_service::on_completion( + overlapped_op* op, DWORD error, DWORD bytes) noexcept +{ + sched_.on_completion(op, error, bytes); +} + +inline void +win_file_service::work_started() noexcept +{ + sched_.work_started(); +} + +inline void +win_file_service::work_finished() noexcept +{ + sched_.work_finished(); +} + +inline void* +win_file_service::iocp_handle() const noexcept +{ + return iocp_; +} + +inline bool +win_file_service::try_flush_data(HANDLE h) noexcept +{ + if (nt_flush_buffers_file_ex_) + { + io_status_block status = {}; + if (nt_flush_buffers_file_ex_( + h, flush_flags_file_data_sync_only, + nullptr, 0, &status) == 0) + return true; + } + return false; +} + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_HAS_IOCP + +#endif // BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_FILE_SERVICE_HPP diff --git a/include/boost/corosio/native/detail/iocp/win_random_access_file.hpp b/include/boost/corosio/native/detail/iocp/win_random_access_file.hpp new file mode 100644 index 000000000..79fefab72 --- /dev/null +++ b/include/boost/corosio/native/detail/iocp/win_random_access_file.hpp @@ -0,0 +1,162 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_RANDOM_ACCESS_FILE_HPP +#define BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_RANDOM_ACCESS_FILE_HPP + +#include + +#if BOOST_COROSIO_HAS_IOCP + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace boost::corosio::detail { + +class win_random_access_file_service; +class win_random_access_file_internal; + +/** Per-operation state for concurrent random-access file IOCP I/O. + + Heap-allocated for each async read/write, enabling unlimited + concurrent operations on the same file. Self-deletes on + completion or shutdown. +*/ +struct raf_concurrent_op + : overlapped_op + , intrusive_list::node +{ + void* buf = nullptr; + DWORD buf_len = 0; + win_random_access_file_internal* file_ = nullptr; + std::shared_ptr file_ref; + + static void do_complete( + void* owner, + scheduler_op* base, + std::uint32_t bytes, + std::uint32_t error); + static void do_cancel_impl(overlapped_op* op) noexcept; + + explicit raf_concurrent_op( + win_random_access_file_internal& f) noexcept; +}; + +/** Internal random-access file state for IOCP-based I/O. + + Each async operation heap-allocates a raf_concurrent_op, + allowing unlimited concurrent reads and writes. +*/ +class win_random_access_file_internal + : public intrusive_list::node + , public std::enable_shared_from_this +{ + friend class win_random_access_file_service; + friend class win_random_access_file; + friend struct raf_concurrent_op; + + win_random_access_file_service& svc_; + win_mutex ops_mutex_; + intrusive_list outstanding_ops_; + HANDLE handle_ = INVALID_HANDLE_VALUE; + +public: + explicit win_random_access_file_internal( + win_random_access_file_service& svc) noexcept; + ~win_random_access_file_internal(); + + std::coroutine_handle<> read_some_at( + std::uint64_t offset, + std::coroutine_handle<>, + capy::executor_ref, + buffer_param, + std::stop_token, + std::error_code*, + std::size_t*); + + std::coroutine_handle<> write_some_at( + std::uint64_t offset, + std::coroutine_handle<>, + capy::executor_ref, + buffer_param, + std::stop_token, + std::error_code*, + std::size_t*); + + HANDLE native_handle() const noexcept; + bool is_open() const noexcept; + void cancel() noexcept; + void close_handle() noexcept; + + std::uint64_t size() const; + void resize(std::uint64_t new_size); + void sync_data(); + void sync_all(); + native_handle_type release(); + void assign(native_handle_type handle); +}; + +/** Random-access file implementation wrapper for IOCP-based I/O. */ +class win_random_access_file final + : public random_access_file::implementation + , public intrusive_list::node +{ + std::shared_ptr internal_; + +public: + explicit win_random_access_file( + std::shared_ptr internal) noexcept; + + void close_internal() noexcept; + + std::coroutine_handle<> read_some_at( + std::uint64_t offset, + std::coroutine_handle<> h, + capy::executor_ref d, + buffer_param buf, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes) override; + + std::coroutine_handle<> write_some_at( + std::uint64_t offset, + std::coroutine_handle<> h, + capy::executor_ref d, + buffer_param buf, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes) override; + + native_handle_type native_handle() const noexcept override; + void cancel() noexcept override; + std::uint64_t size() const override; + void resize(std::uint64_t new_size) override; + void sync_data() override; + void sync_all() override; + native_handle_type release() override; + void assign(native_handle_type handle) override; + + win_random_access_file_internal* get_internal() const noexcept; +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_HAS_IOCP + +#endif // BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_RANDOM_ACCESS_FILE_HPP diff --git a/include/boost/corosio/native/detail/iocp/win_random_access_file_service.hpp b/include/boost/corosio/native/detail/iocp/win_random_access_file_service.hpp new file mode 100644 index 000000000..3ec41ab25 --- /dev/null +++ b/include/boost/corosio/native/detail/iocp/win_random_access_file_service.hpp @@ -0,0 +1,767 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_RANDOM_ACCESS_FILE_SERVICE_HPP +#define BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_RANDOM_ACCESS_FILE_SERVICE_HPP + +#include + +#if BOOST_COROSIO_HAS_IOCP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace boost::corosio::detail { + +/** Windows IOCP random-access file management service. + + Owns all random-access file implementations and coordinates + their lifecycle with the IOCP. + + @par Thread Safety + All public member functions are thread-safe. +*/ +class BOOST_COROSIO_DECL win_random_access_file_service final + : public random_access_file_service +{ +public: + using key_type = win_random_access_file_service; + + explicit win_random_access_file_service(capy::execution_context& ctx); + ~win_random_access_file_service(); + + win_random_access_file_service( + win_random_access_file_service const&) = delete; + win_random_access_file_service& operator=( + win_random_access_file_service const&) = delete; + + io_object::implementation* construct() override; + void destroy(io_object::implementation* p) override; + void close(io_object::handle& h) override; + void shutdown() override; + + std::error_code open_file( + random_access_file::implementation& impl, + std::filesystem::path const& path, + file_base::flags mode) override; + + void destroy_impl(win_random_access_file& impl); + void unregister_impl(win_random_access_file_internal& impl); + + void post(overlapped_op* op); + void on_pending(overlapped_op* op) noexcept; + void on_completion(overlapped_op* op, DWORD error, DWORD bytes) noexcept; + void work_started() noexcept; + void work_finished() noexcept; + + void* iocp_handle() const noexcept; + + /** Attempt data-only flush via NtFlushBuffersFileEx. + + @return true if data-only flush succeeded, false if + caller should fall back to FlushFileBuffers. + */ + bool try_flush_data(HANDLE h) noexcept; + +private: + // NtFlushBuffersFileEx support for data-only sync + struct io_status_block + { + union { LONG Status; void* Pointer; }; + ULONG_PTR Information; + }; + + enum { flush_flags_file_data_sync_only = 4 }; + + using nt_flush_fn = LONG(NTAPI*)( + HANDLE, ULONG, void*, ULONG, io_status_block*); + + win_scheduler& sched_; + win_mutex mutex_; + intrusive_list file_list_; + intrusive_list wrapper_list_; + void* iocp_; + nt_flush_fn nt_flush_buffers_file_ex_; +}; + +/** Get or create the random-access file service for the given context. */ +inline win_random_access_file_service& +get_random_access_file_service(capy::execution_context& ctx, win_scheduler&) +{ + return ctx.make_service(); +} + +// --------------------------------------------------------------------------- +// raf_concurrent_op +// --------------------------------------------------------------------------- + +inline raf_concurrent_op::raf_concurrent_op( + win_random_access_file_internal& f) noexcept + : overlapped_op(&do_complete) + , file_(&f) +{ + cancel_func_ = &do_cancel_impl; +} + +inline void +raf_concurrent_op::do_cancel_impl(overlapped_op* base) noexcept +{ + auto* op = static_cast(base); + op->cancelled.store(true, std::memory_order_release); + if (op->file_->is_open()) + ::CancelIoEx(op->file_->native_handle(), op); +} + +inline void +raf_concurrent_op::do_complete( + void* owner, + scheduler_op* base, + std::uint32_t /*bytes*/, + std::uint32_t /*error*/) +{ + auto* op = static_cast(base); + + if (!owner) + { + // Shutdown path: clean up without invoking + op->stop_cb.reset(); + op->h = {}; + { + std::lock_guard lock(op->file_->ops_mutex_); + op->file_->outstanding_ops_.remove(op); + } + op->file_ref.reset(); + delete op; + return; + } + + // Normal completion + op->stop_cb.reset(); + + if (op->ec_out) + { + if (op->cancelled.load(std::memory_order_acquire)) + *op->ec_out = capy::error::canceled; + else if (op->dwError != 0) + *op->ec_out = make_err(op->dwError); + else if (op->is_read_ && op->bytes_transferred == 0 && !op->empty_buffer) + *op->ec_out = capy::error::eof; + else + *op->ec_out = {}; + } + + if (op->bytes_out) + *op->bytes_out = static_cast(op->bytes_transferred); + + { + std::lock_guard lock(op->file_->ops_mutex_); + op->file_->outstanding_ops_.remove(op); + } + + op->file_ref.reset(); + + auto coro = op->h; + delete op; + coro.resume(); +} + +// --------------------------------------------------------------------------- +// win_random_access_file_internal +// --------------------------------------------------------------------------- + +inline +win_random_access_file_internal::win_random_access_file_internal( + win_random_access_file_service& svc) noexcept + : svc_(svc) +{ +} + +inline +win_random_access_file_internal::~win_random_access_file_internal() +{ + svc_.unregister_impl(*this); +} + +inline HANDLE +win_random_access_file_internal::native_handle() const noexcept +{ + return handle_; +} + +inline bool +win_random_access_file_internal::is_open() const noexcept +{ + return handle_ != INVALID_HANDLE_VALUE; +} + +inline void +win_random_access_file_internal::cancel() noexcept +{ + if (handle_ != INVALID_HANDLE_VALUE) + ::CancelIoEx(handle_, nullptr); + + std::lock_guard lock(ops_mutex_); + outstanding_ops_.for_each([](raf_concurrent_op* op) { + op->request_cancel(); + }); +} + +inline void +win_random_access_file_internal::close_handle() noexcept +{ + if (handle_ != INVALID_HANDLE_VALUE) + { + ::CancelIoEx(handle_, nullptr); + ::CloseHandle(handle_); + handle_ = INVALID_HANDLE_VALUE; + } +} + +inline std::uint64_t +win_random_access_file_internal::size() const +{ + LARGE_INTEGER li; + if (!::GetFileSizeEx(handle_, &li)) + throw_system_error( + make_err(::GetLastError()), "random_access_file::size"); + return static_cast(li.QuadPart); +} + +inline void +win_random_access_file_internal::resize(std::uint64_t new_size) +{ + LARGE_INTEGER li; + li.QuadPart = static_cast(new_size); + if (!::SetFilePointerEx(handle_, li, nullptr, FILE_BEGIN)) + throw_system_error( + make_err(::GetLastError()), "random_access_file::resize"); + if (!::SetEndOfFile(handle_)) + throw_system_error( + make_err(::GetLastError()), "random_access_file::resize"); +} + +inline void +win_random_access_file_internal::sync_data() +{ + // Attempt data-only flush; fall back to full flush + if (svc_.try_flush_data(handle_)) + return; + if (!::FlushFileBuffers(handle_)) + throw_system_error( + make_err(::GetLastError()), "random_access_file::sync_data"); +} + +inline void +win_random_access_file_internal::sync_all() +{ + if (!::FlushFileBuffers(handle_)) + throw_system_error( + make_err(::GetLastError()), "random_access_file::sync_all"); +} + +inline native_handle_type +win_random_access_file_internal::release() +{ + HANDLE h = handle_; + handle_ = INVALID_HANDLE_VALUE; + return reinterpret_cast(h); +} + +inline void +win_random_access_file_internal::assign(native_handle_type handle) +{ + close_handle(); + HANDLE h = reinterpret_cast(handle); + // Register with IOCP so overlapped I/O works + if (!::CreateIoCompletionPort( + h, static_cast(svc_.iocp_handle()), key_io, 0)) + { + throw_system_error( + make_err(::GetLastError()), "random_access_file::assign"); + } + handle_ = h; +} + +inline std::coroutine_handle<> +win_random_access_file_internal::read_some_at( + std::uint64_t offset, + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param param, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) +{ + static constexpr std::size_t max_buffers = 16; + + auto* op = new raf_concurrent_op(*this); + op->file_ref = shared_from_this(); + + op->reset(); + op->is_read_ = true; + op->h = h; + op->ex = ex; + op->ec_out = ec; + op->bytes_out = bytes_out; + op->start(token); + + svc_.work_started(); + + capy::mutable_buffer bufs[max_buffers]; + auto count = param.copy_to(bufs, max_buffers); + + if (count == 0) + { + op->empty_buffer = true; + { + std::lock_guard lock(ops_mutex_); + outstanding_ops_.push_back(op); + } + svc_.on_completion(op, 0, 0); + return std::noop_coroutine(); + } + + op->buf = bufs[0].data(); + op->buf_len = static_cast(bufs[0].size()); + + // Set caller-provided offset in OVERLAPPED + op->Offset = static_cast(offset & 0xFFFFFFFF); + op->OffsetHigh = static_cast(offset >> 32); + + { + std::lock_guard lock(ops_mutex_); + outstanding_ops_.push_back(op); + } + + BOOL ok = ::ReadFile(handle_, op->buf, op->buf_len, nullptr, op); + DWORD err = ok ? 0 : ::GetLastError(); + + if (err != 0 && err != ERROR_IO_PENDING) + { + svc_.on_completion(op, err, 0); + return std::noop_coroutine(); + } + + svc_.on_pending(op); + + // Re-check cancellation after I/O is pending + if (op->cancelled.load(std::memory_order_acquire)) + ::CancelIoEx(handle_, op); + + return std::noop_coroutine(); +} + +inline std::coroutine_handle<> +win_random_access_file_internal::write_some_at( + std::uint64_t offset, + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param param, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) +{ + static constexpr std::size_t max_buffers = 16; + + auto* op = new raf_concurrent_op(*this); + op->file_ref = shared_from_this(); + + op->reset(); + op->is_read_ = false; + op->h = h; + op->ex = ex; + op->ec_out = ec; + op->bytes_out = bytes_out; + op->start(token); + + svc_.work_started(); + + capy::mutable_buffer bufs[max_buffers]; + auto count = param.copy_to(bufs, max_buffers); + + if (count == 0) + { + { + std::lock_guard lock(ops_mutex_); + outstanding_ops_.push_back(op); + } + svc_.on_completion(op, 0, 0); + return std::noop_coroutine(); + } + + op->buf = bufs[0].data(); + op->buf_len = static_cast(bufs[0].size()); + + // Set caller-provided offset in OVERLAPPED + op->Offset = static_cast(offset & 0xFFFFFFFF); + op->OffsetHigh = static_cast(offset >> 32); + + { + std::lock_guard lock(ops_mutex_); + outstanding_ops_.push_back(op); + } + + BOOL ok = ::WriteFile(handle_, op->buf, op->buf_len, nullptr, op); + DWORD err = ok ? 0 : ::GetLastError(); + + if (err != 0 && err != ERROR_IO_PENDING) + { + svc_.on_completion(op, err, 0); + return std::noop_coroutine(); + } + + svc_.on_pending(op); + + // Re-check cancellation after I/O is pending + if (op->cancelled.load(std::memory_order_acquire)) + ::CancelIoEx(handle_, op); + + return std::noop_coroutine(); +} + +// --------------------------------------------------------------------------- +// win_random_access_file wrapper +// --------------------------------------------------------------------------- + +inline +win_random_access_file::win_random_access_file( + std::shared_ptr internal) noexcept + : internal_(std::move(internal)) +{ +} + +inline void +win_random_access_file::close_internal() noexcept +{ + if (internal_) + { + internal_->close_handle(); + internal_.reset(); + } +} + +inline std::coroutine_handle<> +win_random_access_file::read_some_at( + std::uint64_t offset, + std::coroutine_handle<> h, + capy::executor_ref d, + buffer_param buf, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes) +{ + return internal_->read_some_at(offset, h, d, buf, token, ec, bytes); +} + +inline std::coroutine_handle<> +win_random_access_file::write_some_at( + std::uint64_t offset, + std::coroutine_handle<> h, + capy::executor_ref d, + buffer_param buf, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes) +{ + return internal_->write_some_at(offset, h, d, buf, token, ec, bytes); +} + +inline native_handle_type +win_random_access_file::native_handle() const noexcept +{ + return reinterpret_cast(internal_->native_handle()); +} + +inline void +win_random_access_file::cancel() noexcept +{ + internal_->cancel(); +} + +inline std::uint64_t +win_random_access_file::size() const +{ + return internal_->size(); +} + +inline void +win_random_access_file::resize(std::uint64_t new_size) +{ + internal_->resize(new_size); +} + +inline void +win_random_access_file::sync_data() +{ + internal_->sync_data(); +} + +inline void +win_random_access_file::sync_all() +{ + internal_->sync_all(); +} + +inline native_handle_type +win_random_access_file::release() +{ + return internal_->release(); +} + +inline void +win_random_access_file::assign(native_handle_type handle) +{ + internal_->assign(handle); +} + +inline win_random_access_file_internal* +win_random_access_file::get_internal() const noexcept +{ + return internal_.get(); +} + +// --------------------------------------------------------------------------- +// win_random_access_file_service +// --------------------------------------------------------------------------- + +inline +win_random_access_file_service::win_random_access_file_service( + capy::execution_context& ctx) + : sched_(ctx.use_service()) + , iocp_(sched_.native_handle()) + , nt_flush_buffers_file_ex_(nullptr) +{ + if (FARPROC p = ::GetProcAddress( + ::GetModuleHandleA("NTDLL"), "NtFlushBuffersFileEx")) + { + nt_flush_buffers_file_ex_ = reinterpret_cast( + reinterpret_cast(p)); + } +} + +inline +win_random_access_file_service::~win_random_access_file_service() +{ + for (auto* w = wrapper_list_.pop_front(); w != nullptr; + w = wrapper_list_.pop_front()) + delete w; +} + +inline io_object::implementation* +win_random_access_file_service::construct() +{ + auto internal = + std::make_shared(*this); + + { + std::lock_guard lock(mutex_); + file_list_.push_back(internal.get()); + } + + auto* wrapper = new win_random_access_file(std::move(internal)); + + { + std::lock_guard lock(mutex_); + wrapper_list_.push_back(wrapper); + } + + return wrapper; +} + +inline void +win_random_access_file_service::destroy(io_object::implementation* p) +{ + if (p) + { + auto& wrapper = static_cast(*p); + wrapper.close_internal(); + destroy_impl(wrapper); + } +} + +inline void +win_random_access_file_service::close(io_object::handle& h) +{ + auto& wrapper = static_cast(*h.get()); + wrapper.get_internal()->close_handle(); +} + +inline void +win_random_access_file_service::shutdown() +{ + std::lock_guard lock(mutex_); + + for (auto* impl = file_list_.pop_front(); impl != nullptr; + impl = file_list_.pop_front()) + { + impl->close_handle(); + } +} + +inline std::error_code +win_random_access_file_service::open_file( + random_access_file::implementation& impl, + std::filesystem::path const& path, + file_base::flags mode) +{ + // Build access mask + DWORD access = 0; + unsigned a = static_cast(mode) & 3u; + if (a == 3) + access = GENERIC_READ | GENERIC_WRITE; + else if (a == 2) + access = GENERIC_WRITE; + else + access = GENERIC_READ; + + // Build creation disposition + DWORD disposition = OPEN_EXISTING; + if ((mode & file_base::create) && (mode & file_base::exclusive)) + disposition = CREATE_NEW; + else if ((mode & file_base::create) && (mode & file_base::truncate)) + disposition = OPEN_ALWAYS; + else if (mode & file_base::create) + disposition = OPEN_ALWAYS; + else if (mode & file_base::truncate) + disposition = TRUNCATE_EXISTING; + + // Build flags — FILE_FLAG_OVERLAPPED + FILE_FLAG_RANDOM_ACCESS + DWORD flags = FILE_ATTRIBUTE_NORMAL + | FILE_FLAG_OVERLAPPED + | FILE_FLAG_RANDOM_ACCESS; + if (mode & file_base::sync_all_on_write) + flags |= FILE_FLAG_WRITE_THROUGH; + + HANDLE h = ::CreateFileW( + path.c_str(), + access, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, + disposition, + flags, + nullptr); + + if (h == INVALID_HANDLE_VALUE) + return make_err(::GetLastError()); + + // Register with IOCP + if (!::CreateIoCompletionPort( + h, static_cast(iocp_), key_io, 0)) + { + DWORD err = ::GetLastError(); + ::CloseHandle(h); + return make_err(err); + } + + // Handle truncation for create|truncate combo + if ((mode & file_base::create) && (mode & file_base::truncate) + && disposition == OPEN_ALWAYS) + { + if (!::SetEndOfFile(h)) + { + DWORD err = ::GetLastError(); + ::CloseHandle(h); + return make_err(err); + } + } + + auto& internal = + *static_cast(impl).get_internal(); + internal.handle_ = h; + + return {}; +} + +inline void +win_random_access_file_service::destroy_impl(win_random_access_file& impl) +{ + { + std::lock_guard lock(mutex_); + wrapper_list_.remove(&impl); + } + delete &impl; +} + +inline void +win_random_access_file_service::unregister_impl( + win_random_access_file_internal& impl) +{ + std::lock_guard lock(mutex_); + file_list_.remove(&impl); +} + +inline void +win_random_access_file_service::post(overlapped_op* op) +{ + sched_.post(op); +} + +inline void +win_random_access_file_service::on_pending(overlapped_op* op) noexcept +{ + sched_.on_pending(op); +} + +inline void +win_random_access_file_service::on_completion( + overlapped_op* op, DWORD error, DWORD bytes) noexcept +{ + sched_.on_completion(op, error, bytes); +} + +inline void +win_random_access_file_service::work_started() noexcept +{ + sched_.work_started(); +} + +inline void +win_random_access_file_service::work_finished() noexcept +{ + sched_.work_finished(); +} + +inline void* +win_random_access_file_service::iocp_handle() const noexcept +{ + return iocp_; +} + +inline bool +win_random_access_file_service::try_flush_data(HANDLE h) noexcept +{ + if (nt_flush_buffers_file_ex_) + { + io_status_block status = {}; + if (nt_flush_buffers_file_ex_( + h, flush_flags_file_data_sync_only, + nullptr, 0, &status) == 0) + return true; + } + return false; +} + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_HAS_IOCP + +#endif // BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_RANDOM_ACCESS_FILE_SERVICE_HPP diff --git a/include/boost/corosio/native/detail/iocp/win_stream_file.hpp b/include/boost/corosio/native/detail/iocp/win_stream_file.hpp new file mode 100644 index 000000000..d9ad1fff6 --- /dev/null +++ b/include/boost/corosio/native/detail/iocp/win_stream_file.hpp @@ -0,0 +1,176 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_STREAM_FILE_HPP +#define BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_STREAM_FILE_HPP + +#include + +#if BOOST_COROSIO_HAS_IOCP + +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace boost::corosio::detail { + +class win_file_service; +class win_stream_file_internal; + +/** Read operation state for stream file IOCP I/O. */ +struct file_read_op : overlapped_op +{ + void* buf = nullptr; + DWORD buf_len = 0; + win_stream_file_internal& file_; + std::shared_ptr file_ptr; + + static void do_complete( + void* owner, + scheduler_op* base, + std::uint32_t bytes, + std::uint32_t error); + static void do_cancel_impl(overlapped_op* op) noexcept; + + explicit file_read_op(win_stream_file_internal& f) noexcept; +}; + +/** Write operation state for stream file IOCP I/O. */ +struct file_write_op : overlapped_op +{ + void* buf = nullptr; + DWORD buf_len = 0; + win_stream_file_internal& file_; + std::shared_ptr file_ptr; + + static void do_complete( + void* owner, + scheduler_op* base, + std::uint32_t bytes, + std::uint32_t error); + static void do_cancel_impl(overlapped_op* op) noexcept; + + explicit file_write_op(win_stream_file_internal& f) noexcept; +}; + +/** Internal stream file state for IOCP-based I/O. + + Contains the actual state for a single file, including + the native handle, position tracking, and pending operations. + Derives from enable_shared_from_this so operations can extend + its lifetime. +*/ +class win_stream_file_internal + : public intrusive_list::node + , public std::enable_shared_from_this +{ + friend class win_file_service; + friend class win_stream_file; + friend struct file_read_op; + friend struct file_write_op; + + win_file_service& svc_; + file_read_op rd_; + file_write_op wr_; + HANDLE handle_ = INVALID_HANDLE_VALUE; + std::uint64_t offset_ = 0; + +public: + explicit win_stream_file_internal(win_file_service& svc) noexcept; + ~win_stream_file_internal(); + + std::coroutine_handle<> read_some( + std::coroutine_handle<>, + capy::executor_ref, + buffer_param, + std::stop_token, + std::error_code*, + std::size_t*); + + std::coroutine_handle<> write_some( + std::coroutine_handle<>, + capy::executor_ref, + buffer_param, + std::stop_token, + std::error_code*, + std::size_t*); + + HANDLE native_handle() const noexcept; + bool is_open() const noexcept; + void cancel() noexcept; + void close_handle() noexcept; + + std::uint64_t size() const; + void resize(std::uint64_t new_size); + void sync_data(); + void sync_all(); + native_handle_type release(); + void assign(native_handle_type handle); + std::uint64_t seek(std::int64_t offset, file_base::seek_basis origin); +}; + +/** Stream file implementation wrapper for IOCP-based I/O. + + Public-facing implementation that holds a shared_ptr to + the internal state. Delegates all virtual calls. +*/ +class win_stream_file final + : public stream_file::implementation + , public intrusive_list::node +{ + std::shared_ptr internal_; + +public: + explicit win_stream_file( + std::shared_ptr internal) noexcept; + + void close_internal() noexcept; + + std::coroutine_handle<> read_some( + std::coroutine_handle<> h, + capy::executor_ref d, + buffer_param buf, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes) override; + + std::coroutine_handle<> write_some( + std::coroutine_handle<> h, + capy::executor_ref d, + buffer_param buf, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes) override; + + native_handle_type native_handle() const noexcept override; + void cancel() noexcept override; + std::uint64_t size() const override; + void resize(std::uint64_t new_size) override; + void sync_data() override; + void sync_all() override; + native_handle_type release() override; + void assign(native_handle_type handle) override; + std::uint64_t seek(std::int64_t offset, file_base::seek_basis origin) override; + + win_stream_file_internal* get_internal() const noexcept; +}; + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_HAS_IOCP + +#endif // BOOST_COROSIO_NATIVE_DETAIL_IOCP_WIN_STREAM_FILE_HPP diff --git a/include/boost/corosio/native/detail/kqueue/kqueue_scheduler.hpp b/include/boost/corosio/native/detail/kqueue/kqueue_scheduler.hpp index f829aeeae..a16a46b0c 100644 --- a/include/boost/corosio/native/detail/kqueue/kqueue_scheduler.hpp +++ b/include/boost/corosio/native/detail/kqueue/kqueue_scheduler.hpp @@ -25,6 +25,8 @@ #include #include #include +#include +#include #include @@ -178,6 +180,8 @@ inline kqueue_scheduler::kqueue_scheduler(capy::execution_context& ctx, int) get_resolver_service(ctx, *this); get_signal_service(ctx, *this); + get_stream_file_service(ctx, *this); + get_random_access_file_service(ctx, *this); completed_ops_.push(&task_op_); } diff --git a/include/boost/corosio/native/detail/posix/posix_random_access_file.hpp b/include/boost/corosio/native/detail/posix/posix_random_access_file.hpp new file mode 100644 index 000000000..3f3087fae --- /dev/null +++ b/include/boost/corosio/native/detail/posix/posix_random_access_file.hpp @@ -0,0 +1,355 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_RANDOM_ACCESS_FILE_HPP +#define BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_RANDOM_ACCESS_FILE_HPP + +#include + +#if BOOST_COROSIO_POSIX + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +/* + POSIX Random-Access File Implementation + ======================================== + + Each async read/write heap-allocates an raf_op that serves + as both the thread-pool work item and the scheduler completion + op. This allows unlimited concurrent operations on the same + file object, matching Asio's per-op allocation model. + + The raf_op self-deletes on completion or shutdown. +*/ + +namespace boost::corosio::detail { + +struct scheduler; +class posix_random_access_file_service; + +/** Random-access file implementation for POSIX backends. */ +class posix_random_access_file final + : public random_access_file::implementation + , public std::enable_shared_from_this + , public intrusive_list::node +{ + friend class posix_random_access_file_service; + +public: + static constexpr std::size_t max_buffers = 16; + + /** Per-operation state, heap-allocated for each async call. + + Inherits from scheduler_op (for scheduler completion) and + pool_work_item (for thread-pool dispatch). Linked into the + file's outstanding_ops_ list for cancellation tracking. + */ + struct raf_op final + : scheduler_op + , pool_work_item + , intrusive_list::node + { + struct canceller + { + raf_op* op; + void operator()() const noexcept + { + op->cancelled.store(true, std::memory_order_release); + } + }; + + std::coroutine_handle<> h; + capy::executor_ref ex; + + std::error_code* ec_out = nullptr; + std::size_t* bytes_out = nullptr; + + iovec iovecs[max_buffers]; + int iovec_count = 0; + std::uint64_t offset = 0; + + int errn = 0; + std::size_t bytes_transferred = 0; + bool is_read = false; + + std::atomic cancelled{false}; + std::optional> stop_cb; + + posix_random_access_file* file_ = nullptr; + std::shared_ptr file_ref; + + void start(std::stop_token const& token) + { + cancelled.store(false, std::memory_order_release); + stop_cb.reset(); + if (token.stop_possible()) + stop_cb.emplace(token, canceller{this}); + } + + void operator()() override; + void destroy() override; + + /// Thread-pool work function: executes preadv/pwritev. + static void do_work(pool_work_item*) noexcept; + }; + + explicit posix_random_access_file( + posix_random_access_file_service& svc) noexcept; + + // -- random_access_file::implementation -- + + std::coroutine_handle<> read_some_at( + std::uint64_t offset, + std::coroutine_handle<>, + capy::executor_ref, + buffer_param, + std::stop_token, + std::error_code*, + std::size_t*) override; + + std::coroutine_handle<> write_some_at( + std::uint64_t offset, + std::coroutine_handle<>, + capy::executor_ref, + buffer_param, + std::stop_token, + std::error_code*, + std::size_t*) override; + + native_handle_type native_handle() const noexcept override + { + return fd_; + } + + void cancel() noexcept override + { + std::lock_guard lock(ops_mutex_); + outstanding_ops_.for_each([](raf_op* op) { + op->cancelled.store(true, std::memory_order_release); + }); + } + + std::uint64_t size() const override; + void resize(std::uint64_t new_size) override; + void sync_data() override; + void sync_all() override; + native_handle_type release() override; + void assign(native_handle_type handle) override; + + std::error_code open_file( + std::filesystem::path const& path, file_base::flags mode); + void close_file() noexcept; + +private: + posix_random_access_file_service& svc_; + int fd_ = -1; + std::mutex ops_mutex_; + intrusive_list outstanding_ops_; +}; + +// --------------------------------------------------------------------------- +// Inline implementation +// --------------------------------------------------------------------------- + +inline +posix_random_access_file::posix_random_access_file( + posix_random_access_file_service& svc) noexcept + : svc_(svc) +{ +} + +inline std::error_code +posix_random_access_file::open_file( + std::filesystem::path const& path, file_base::flags mode) +{ + close_file(); + + int oflags = 0; + + unsigned access = static_cast(mode) & 3u; + if (access == static_cast(file_base::read_write)) + oflags |= O_RDWR; + else if (access == static_cast(file_base::write_only)) + oflags |= O_WRONLY; + else + oflags |= O_RDONLY; + + if ((mode & file_base::create) != file_base::flags(0)) + oflags |= O_CREAT; + if ((mode & file_base::exclusive) != file_base::flags(0)) + oflags |= O_EXCL; + if ((mode & file_base::truncate) != file_base::flags(0)) + oflags |= O_TRUNC; + if ((mode & file_base::sync_all_on_write) != file_base::flags(0)) + oflags |= O_SYNC; + // Note: no O_APPEND for random access files + + int fd = ::open(path.c_str(), oflags, 0666); + if (fd < 0) + return make_err(errno); + + fd_ = fd; + +#ifdef POSIX_FADV_RANDOM + ::posix_fadvise(fd_, 0, 0, POSIX_FADV_RANDOM); +#endif + + return {}; +} + +inline void +posix_random_access_file::close_file() noexcept +{ + if (fd_ >= 0) + { + ::close(fd_); + fd_ = -1; + } +} + +inline std::uint64_t +posix_random_access_file::size() const +{ + struct stat st; + if (::fstat(fd_, &st) < 0) + throw_system_error(make_err(errno), "random_access_file::size"); + return static_cast(st.st_size); +} + +inline void +posix_random_access_file::resize(std::uint64_t new_size) +{ + if (new_size > static_cast(std::numeric_limits::max())) + throw_system_error(make_err(EOVERFLOW), "random_access_file::resize"); + if (::ftruncate(fd_, static_cast(new_size)) < 0) + throw_system_error(make_err(errno), "random_access_file::resize"); +} + +inline void +posix_random_access_file::sync_data() +{ +#if BOOST_COROSIO_HAS_POSIX_SYNCHRONIZED_IO + if (::fdatasync(fd_) < 0) +#else // BOOST_COROSIO_HAS_POSIX_SYNCHRONIZED_IO + if (::fsync(fd_) < 0) +#endif // BOOST_COROSIO_HAS_POSIX_SYNCHRONIZED_IO + throw_system_error(make_err(errno), "random_access_file::sync_data"); +} + +inline void +posix_random_access_file::sync_all() +{ + if (::fsync(fd_) < 0) + throw_system_error(make_err(errno), "random_access_file::sync_all"); +} + +inline native_handle_type +posix_random_access_file::release() +{ + int fd = fd_; + fd_ = -1; + return fd; +} + +inline void +posix_random_access_file::assign(native_handle_type handle) +{ + close_file(); + fd_ = handle; +} + +// read_some_at, write_some_at are defined in +// posix_random_access_file_service.hpp after the service. + +// -- raf_op completion handler (scheduler thread) -- + +inline void +posix_random_access_file::raf_op::operator()() +{ + stop_cb.reset(); + + bool const was_cancelled = cancelled.load(std::memory_order_acquire); + + if (ec_out) + { + if (was_cancelled) + *ec_out = capy::error::canceled; + else if (errn != 0) + *ec_out = make_err(errn); + else if (is_read && bytes_transferred == 0) + *ec_out = capy::error::eof; + else + *ec_out = {}; + } + + if (bytes_out) + *bytes_out = was_cancelled ? 0 : bytes_transferred; + + { + std::lock_guard lock(file_->ops_mutex_); + file_->outstanding_ops_.remove(this); + } + + file_ref.reset(); + + auto coro = h; + ex.on_work_finished(); + delete this; + coro.resume(); +} + +// -- raf_op shutdown cleanup -- + +inline void +posix_random_access_file::raf_op::destroy() +{ + stop_cb.reset(); + { + std::lock_guard lock(file_->ops_mutex_); + file_->outstanding_ops_.remove(this); + } + file_ref.reset(); + ex.on_work_finished(); + delete this; +} + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_POSIX + +#endif // BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_RANDOM_ACCESS_FILE_HPP diff --git a/include/boost/corosio/native/detail/posix/posix_random_access_file_service.hpp b/include/boost/corosio/native/detail/posix/posix_random_access_file_service.hpp new file mode 100644 index 000000000..6802108bb --- /dev/null +++ b/include/boost/corosio/native/detail/posix/posix_random_access_file_service.hpp @@ -0,0 +1,326 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_RANDOM_ACCESS_FILE_SERVICE_HPP +#define BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_RANDOM_ACCESS_FILE_SERVICE_HPP + +#include + +#if BOOST_COROSIO_POSIX + +#include +#include +#include + +#include +#include +#include + +namespace boost::corosio::detail { + +/** Random-access file service for POSIX backends. */ +class BOOST_COROSIO_DECL posix_random_access_file_service final + : public random_access_file_service +{ +public: + posix_random_access_file_service( + capy::execution_context& ctx, scheduler& sched) + : sched_(&sched) + , pool_(get_or_create_pool(ctx)) + { + } + + ~posix_random_access_file_service() override = default; + + posix_random_access_file_service( + posix_random_access_file_service const&) = delete; + posix_random_access_file_service& operator=( + posix_random_access_file_service const&) = delete; + + io_object::implementation* construct() override + { + auto ptr = std::make_shared(*this); + auto* impl = ptr.get(); + + { + std::lock_guard lock(mutex_); + file_list_.push_back(impl); + file_ptrs_[impl] = std::move(ptr); + } + + return impl; + } + + void destroy(io_object::implementation* p) override + { + auto& impl = static_cast(*p); + impl.cancel(); + impl.close_file(); + destroy_impl(impl); + } + + void close(io_object::handle& h) override + { + if (h.get()) + { + auto& impl = static_cast(*h.get()); + impl.cancel(); + impl.close_file(); + } + } + + std::error_code open_file( + random_access_file::implementation& impl, + std::filesystem::path const& path, + file_base::flags mode) override + { + return static_cast(impl).open_file( + path, mode); + } + + void shutdown() override + { + std::lock_guard lock(mutex_); + for (auto* impl = file_list_.pop_front(); impl != nullptr; + impl = file_list_.pop_front()) + { + impl->cancel(); + impl->close_file(); + } + file_ptrs_.clear(); + } + + void destroy_impl(posix_random_access_file& impl) + { + std::lock_guard lock(mutex_); + file_list_.remove(&impl); + file_ptrs_.erase(&impl); + } + + void post(scheduler_op* op) + { + sched_->post(op); + } + + void work_started() noexcept + { + sched_->work_started(); + } + + void work_finished() noexcept + { + sched_->work_finished(); + } + + thread_pool& pool() noexcept + { + return pool_; + } + +private: + static thread_pool& get_or_create_pool(capy::execution_context& ctx) + { + auto* p = ctx.find_service(); + if (p) + return *p; + return ctx.make_service(); + } + + scheduler* sched_; + thread_pool& pool_; + std::mutex mutex_; + intrusive_list file_list_; + std::unordered_map< + posix_random_access_file*, + std::shared_ptr> + file_ptrs_; +}; + +/** Get or create the random-access file service for the given context. */ +inline posix_random_access_file_service& +get_random_access_file_service(capy::execution_context& ctx, scheduler& sched) +{ + return ctx.make_service(sched); +} + +// --------------------------------------------------------------------------- +// posix_random_access_file inline implementations (require complete service) +// --------------------------------------------------------------------------- + +inline std::coroutine_handle<> +posix_random_access_file::read_some_at( + std::uint64_t offset, + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param param, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) +{ + capy::mutable_buffer bufs[max_buffers]; + auto count = param.copy_to(bufs, max_buffers); + + if (count == 0) + { + *ec = {}; + *bytes_out = 0; + return h; + } + + auto* op = new raf_op(); + op->is_read = true; + op->offset = offset; + + op->iovec_count = static_cast(count); + for (int i = 0; i < op->iovec_count; ++i) + { + op->iovecs[i].iov_base = bufs[i].data(); + op->iovecs[i].iov_len = bufs[i].size(); + } + + op->h = h; + op->ex = ex; + op->ec_out = ec; + op->bytes_out = bytes_out; + op->file_ = this; + op->file_ref = this->shared_from_this(); + op->start(token); + + op->ex.on_work_started(); + + { + std::lock_guard lock(ops_mutex_); + outstanding_ops_.push_back(op); + } + + static_cast(op)->func_ = &raf_op::do_work; + if (!svc_.pool().post(static_cast(op))) + { + op->cancelled.store(true, std::memory_order_release); + svc_.post(static_cast(op)); + } + return std::noop_coroutine(); +} + +inline std::coroutine_handle<> +posix_random_access_file::write_some_at( + std::uint64_t offset, + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param param, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) +{ + capy::mutable_buffer bufs[max_buffers]; + auto count = param.copy_to(bufs, max_buffers); + + if (count == 0) + { + *ec = {}; + *bytes_out = 0; + return h; + } + + auto* op = new raf_op(); + op->is_read = false; + op->offset = offset; + + op->iovec_count = static_cast(count); + for (int i = 0; i < op->iovec_count; ++i) + { + op->iovecs[i].iov_base = bufs[i].data(); + op->iovecs[i].iov_len = bufs[i].size(); + } + + op->h = h; + op->ex = ex; + op->ec_out = ec; + op->bytes_out = bytes_out; + op->file_ = this; + op->file_ref = this->shared_from_this(); + op->start(token); + + op->ex.on_work_started(); + + { + std::lock_guard lock(ops_mutex_); + outstanding_ops_.push_back(op); + } + + static_cast(op)->func_ = &raf_op::do_work; + if (!svc_.pool().post(static_cast(op))) + { + op->cancelled.store(true, std::memory_order_release); + svc_.post(static_cast(op)); + } + return std::noop_coroutine(); +} + +// -- raf_op thread-pool work function -- + +inline void +posix_random_access_file::raf_op::do_work(pool_work_item* w) noexcept +{ + auto* op = static_cast(w); + auto* self = op->file_; + + if (op->cancelled.load(std::memory_order_acquire)) + { + op->errn = ECANCELED; + op->bytes_transferred = 0; + } + else if (op->offset > + static_cast(std::numeric_limits::max())) + { + op->errn = EOVERFLOW; + op->bytes_transferred = 0; + } + else + { + ssize_t n; + if (op->is_read) + { + do + { + n = ::preadv(self->fd_, op->iovecs, op->iovec_count, + static_cast(op->offset)); + } + while (n < 0 && errno == EINTR); + } + else + { + do + { + n = ::pwritev(self->fd_, op->iovecs, op->iovec_count, + static_cast(op->offset)); + } + while (n < 0 && errno == EINTR); + } + + if (n >= 0) + { + op->errn = 0; + op->bytes_transferred = static_cast(n); + } + else + { + op->errn = errno; + op->bytes_transferred = 0; + } + } + + self->svc_.post(static_cast(op)); +} + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_POSIX + +#endif // BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_RANDOM_ACCESS_FILE_SERVICE_HPP diff --git a/include/boost/corosio/native/detail/posix/posix_stream_file.hpp b/include/boost/corosio/native/detail/posix/posix_stream_file.hpp new file mode 100644 index 000000000..a83a1febc --- /dev/null +++ b/include/boost/corosio/native/detail/posix/posix_stream_file.hpp @@ -0,0 +1,446 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_STREAM_FILE_HPP +#define BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_STREAM_FILE_HPP + +#include + +#if BOOST_COROSIO_POSIX + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include + +/* + POSIX Stream File Implementation + ================================= + + Regular files cannot be monitored by epoll/kqueue/select — the kernel + always reports them as ready. Blocking I/O (pread/pwrite) is dispatched + to a shared thread pool, with completion posted back to the scheduler. + + This follows the same pattern as posix_resolver: pool_work_item for + dispatch, scheduler_op for completion, shared_from_this for lifetime. + + Completion Flow + --------------- + 1. read_some() sets up file_read_op, posts to thread pool + 2. Pool thread runs preadv() (blocking) + 3. Pool thread stores results, posts scheduler_op to scheduler + 4. Scheduler invokes op() which resumes the coroutine + + Single-Inflight Constraint + -------------------------- + Only one asynchronous operation may be in flight at a time on a + given file object. Concurrent read and write is not supported + because both share offset_ without synchronization. +*/ + +namespace boost::corosio::detail { + +struct scheduler; +class posix_stream_file_service; + +/** Stream file implementation for POSIX backends. + + Each instance contains embedded operation objects (read_op_, write_op_) + that are reused across calls. This avoids per-operation heap allocation. +*/ +class posix_stream_file final + : public stream_file::implementation + , public std::enable_shared_from_this + , public intrusive_list::node +{ + friend class posix_stream_file_service; + +public: + static constexpr std::size_t max_buffers = 16; + + /** Operation state for a single file read or write. */ + struct file_op : scheduler_op + { + struct canceller + { + file_op* op; + void operator()() const noexcept + { + op->request_cancel(); + } + }; + + // Coroutine state + std::coroutine_handle<> h; + detail::continuation_op cont_op; + capy::executor_ref ex; + + // Output pointers + std::error_code* ec_out = nullptr; + std::size_t* bytes_out = nullptr; + + // Buffer data (copied from buffer_param at submission time) + iovec iovecs[max_buffers]; + int iovec_count = 0; + + // Result storage (populated by worker thread) + int errn = 0; + std::size_t bytes_transferred = 0; + bool is_read = false; + + // Thread coordination + std::atomic cancelled{false}; + std::optional> stop_cb; + + /// Prevents use-after-free when file is closed with pending ops. + std::shared_ptr impl_ref; + + file_op() = default; + + void reset() noexcept + { + iovec_count = 0; + errn = 0; + bytes_transferred = 0; + is_read = false; + cancelled.store(false, std::memory_order_relaxed); + stop_cb.reset(); + impl_ref.reset(); + ec_out = nullptr; + bytes_out = nullptr; + } + + void operator()() override; + void destroy() override; + + void request_cancel() noexcept + { + cancelled.store(true, std::memory_order_release); + } + + void start(std::stop_token const& token) + { + cancelled.store(false, std::memory_order_release); + stop_cb.reset(); + if (token.stop_possible()) + stop_cb.emplace(token, canceller{this}); + } + }; + + /** Pool work item for thread pool dispatch. */ + struct pool_op : pool_work_item + { + posix_stream_file* file_ = nullptr; + std::shared_ptr ref_; + }; + + explicit posix_stream_file(posix_stream_file_service& svc) noexcept; + + // -- io_stream::implementation -- + + std::coroutine_handle<> read_some( + std::coroutine_handle<>, + capy::executor_ref, + buffer_param, + std::stop_token, + std::error_code*, + std::size_t*) override; + + std::coroutine_handle<> write_some( + std::coroutine_handle<>, + capy::executor_ref, + buffer_param, + std::stop_token, + std::error_code*, + std::size_t*) override; + + // -- stream_file::implementation -- + + native_handle_type native_handle() const noexcept override + { + return fd_; + } + + void cancel() noexcept override + { + read_op_.request_cancel(); + write_op_.request_cancel(); + } + + std::uint64_t size() const override; + void resize(std::uint64_t new_size) override; + void sync_data() override; + void sync_all() override; + native_handle_type release() override; + void assign(native_handle_type handle) override; + std::uint64_t seek(std::int64_t offset, file_base::seek_basis origin) override; + + // -- Internal -- + + /** Open the file and store the fd. */ + std::error_code open_file( + std::filesystem::path const& path, file_base::flags mode); + + /** Close the file descriptor. */ + void close_file() noexcept; + +private: + posix_stream_file_service& svc_; + int fd_ = -1; + std::uint64_t offset_ = 0; + + file_op read_op_; + file_op write_op_; + pool_op read_pool_op_; + pool_op write_pool_op_; + + static void do_read_work(pool_work_item*) noexcept; + static void do_write_work(pool_work_item*) noexcept; +}; + +// --------------------------------------------------------------------------- +// Inline implementation +// --------------------------------------------------------------------------- + +inline +posix_stream_file::posix_stream_file(posix_stream_file_service& svc) noexcept + : svc_(svc) +{ +} + +inline std::error_code +posix_stream_file::open_file( + std::filesystem::path const& path, file_base::flags mode) +{ + close_file(); + + int oflags = 0; + + // Access mode + unsigned access = static_cast(mode) & 3u; + if (access == static_cast(file_base::read_write)) + oflags |= O_RDWR; + else if (access == static_cast(file_base::write_only)) + oflags |= O_WRONLY; + else + oflags |= O_RDONLY; + + // Creation flags + if ((mode & file_base::create) != file_base::flags(0)) + oflags |= O_CREAT; + if ((mode & file_base::exclusive) != file_base::flags(0)) + oflags |= O_EXCL; + if ((mode & file_base::truncate) != file_base::flags(0)) + oflags |= O_TRUNC; + if ((mode & file_base::append) != file_base::flags(0)) + oflags |= O_APPEND; + if ((mode & file_base::sync_all_on_write) != file_base::flags(0)) + oflags |= O_SYNC; + + int fd = ::open(path.c_str(), oflags, 0666); + if (fd < 0) + return make_err(errno); + + fd_ = fd; + offset_ = 0; + + // Append mode: position at end-of-file (preadv/pwritev use + // explicit offsets, so O_APPEND alone is not sufficient). + if ((mode & file_base::append) != file_base::flags(0)) + { + struct stat st; + if (::fstat(fd, &st) < 0) + { + int err = errno; + ::close(fd); + fd_ = -1; + return make_err(err); + } + offset_ = static_cast(st.st_size); + } + +#ifdef POSIX_FADV_SEQUENTIAL + ::posix_fadvise(fd_, 0, 0, POSIX_FADV_SEQUENTIAL); +#endif + + return {}; +} + +inline void +posix_stream_file::close_file() noexcept +{ + if (fd_ >= 0) + { + ::close(fd_); + fd_ = -1; + } +} + +inline std::uint64_t +posix_stream_file::size() const +{ + struct stat st; + if (::fstat(fd_, &st) < 0) + throw_system_error(make_err(errno), "stream_file::size"); + return static_cast(st.st_size); +} + +inline void +posix_stream_file::resize(std::uint64_t new_size) +{ + if (new_size > static_cast(std::numeric_limits::max())) + throw_system_error(make_err(EOVERFLOW), "stream_file::resize"); + if (::ftruncate(fd_, static_cast(new_size)) < 0) + throw_system_error(make_err(errno), "stream_file::resize"); +} + +inline void +posix_stream_file::sync_data() +{ +#if BOOST_COROSIO_HAS_POSIX_SYNCHRONIZED_IO + if (::fdatasync(fd_) < 0) +#else // BOOST_COROSIO_HAS_POSIX_SYNCHRONIZED_IO + if (::fsync(fd_) < 0) +#endif // BOOST_COROSIO_HAS_POSIX_SYNCHRONIZED_IO + throw_system_error(make_err(errno), "stream_file::sync_data"); +} + +inline void +posix_stream_file::sync_all() +{ + if (::fsync(fd_) < 0) + throw_system_error(make_err(errno), "stream_file::sync_all"); +} + +inline native_handle_type +posix_stream_file::release() +{ + int fd = fd_; + fd_ = -1; + offset_ = 0; + return fd; +} + +inline void +posix_stream_file::assign(native_handle_type handle) +{ + close_file(); + fd_ = handle; + offset_ = 0; +} + +inline std::uint64_t +posix_stream_file::seek(std::int64_t offset, file_base::seek_basis origin) +{ + // We track offset_ ourselves (not the kernel fd offset) + // because preadv/pwritev use explicit offsets. + std::int64_t new_pos; + + if (origin == file_base::seek_set) + { + new_pos = offset; + } + else if (origin == file_base::seek_cur) + { + new_pos = static_cast(offset_) + offset; + } + else + { + struct stat st; + if (::fstat(fd_, &st) < 0) + throw_system_error(make_err(errno), "stream_file::seek"); + new_pos = st.st_size + offset; + } + + if (new_pos < 0) + throw_system_error(make_err(EINVAL), "stream_file::seek"); + if (new_pos > static_cast(std::numeric_limits::max())) + throw_system_error(make_err(EOVERFLOW), "stream_file::seek"); + + offset_ = static_cast(new_pos); + + return offset_; +} + +// -- file_op completion handler -- +// (read_some, write_some, do_read_work, do_write_work are +// defined in posix_stream_file_service.hpp after the service) + +inline void +posix_stream_file::file_op::operator()() +{ + stop_cb.reset(); + + bool const was_cancelled = cancelled.load(std::memory_order_acquire); + + if (ec_out) + { + if (was_cancelled) + *ec_out = capy::error::canceled; + else if (errn != 0) + *ec_out = make_err(errn); + else if (is_read && bytes_transferred == 0) + *ec_out = capy::error::eof; + else + *ec_out = {}; + } + + if (bytes_out) + *bytes_out = was_cancelled ? 0 : bytes_transferred; + + // Move impl_ref to a local so members remain valid through + // dispatch — impl_ref may be the last shared_ptr keeping + // the parent posix_stream_file (which embeds this file_op) alive. + auto prevent_destroy = std::move(impl_ref); + ex.on_work_finished(); + cont_op.cont.h = h; + dispatch_coro(ex, cont_op.cont).resume(); +} + +inline void +posix_stream_file::file_op::destroy() +{ + stop_cb.reset(); + auto local_ex = ex; + impl_ref.reset(); + local_ex.on_work_finished(); +} + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_POSIX + +#endif // BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_STREAM_FILE_HPP diff --git a/include/boost/corosio/native/detail/posix/posix_stream_file_service.hpp b/include/boost/corosio/native/detail/posix/posix_stream_file_service.hpp new file mode 100644 index 000000000..ef13807c1 --- /dev/null +++ b/include/boost/corosio/native/detail/posix/posix_stream_file_service.hpp @@ -0,0 +1,326 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_STREAM_FILE_SERVICE_HPP +#define BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_STREAM_FILE_SERVICE_HPP + +#include + +#if BOOST_COROSIO_POSIX + +#include +#include +#include + +#include +#include + +namespace boost::corosio::detail { + +/** Stream file service for POSIX backends. + + Owns all posix_stream_file instances. Thread lifecycle is + managed by the thread_pool service (shared with resolver). +*/ +class BOOST_COROSIO_DECL posix_stream_file_service final + : public file_service +{ +public: + posix_stream_file_service( + capy::execution_context& ctx, scheduler& sched) + : sched_(&sched) + , pool_(get_or_create_pool(ctx)) + { + } + + ~posix_stream_file_service() override = default; + + posix_stream_file_service(posix_stream_file_service const&) = delete; + posix_stream_file_service& operator=(posix_stream_file_service const&) = delete; + + io_object::implementation* construct() override + { + auto ptr = std::make_shared(*this); + auto* impl = ptr.get(); + + { + std::lock_guard lock(mutex_); + file_list_.push_back(impl); + file_ptrs_[impl] = std::move(ptr); + } + + return impl; + } + + void destroy(io_object::implementation* p) override + { + auto& impl = static_cast(*p); + impl.cancel(); + impl.close_file(); + destroy_impl(impl); + } + + void close(io_object::handle& h) override + { + if (h.get()) + { + auto& impl = static_cast(*h.get()); + impl.cancel(); + impl.close_file(); + } + } + + std::error_code open_file( + stream_file::implementation& impl, + std::filesystem::path const& path, + file_base::flags mode) override + { + return static_cast(impl).open_file(path, mode); + } + + void shutdown() override + { + std::lock_guard lock(mutex_); + for (auto* impl = file_list_.pop_front(); impl != nullptr; + impl = file_list_.pop_front()) + { + impl->cancel(); + impl->close_file(); + } + file_ptrs_.clear(); + } + + void destroy_impl(posix_stream_file& impl) + { + std::lock_guard lock(mutex_); + file_list_.remove(&impl); + file_ptrs_.erase(&impl); + } + + void post(scheduler_op* op) + { + sched_->post(op); + } + + void work_started() noexcept + { + sched_->work_started(); + } + + void work_finished() noexcept + { + sched_->work_finished(); + } + + thread_pool& pool() noexcept + { + return pool_; + } + +private: + static thread_pool& get_or_create_pool(capy::execution_context& ctx) + { + auto* p = ctx.find_service(); + if (p) + return *p; + return ctx.make_service(); + } + + scheduler* sched_; + thread_pool& pool_; + std::mutex mutex_; + intrusive_list file_list_; + std::unordered_map> + file_ptrs_; +}; + +/** Get or create the stream file service for the given context. */ +inline posix_stream_file_service& +get_stream_file_service(capy::execution_context& ctx, scheduler& sched) +{ + return ctx.make_service(sched); +} + +// --------------------------------------------------------------------------- +// posix_stream_file inline implementations (require complete service type) +// --------------------------------------------------------------------------- + +inline std::coroutine_handle<> +posix_stream_file::read_some( + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param param, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) +{ + auto& op = read_op_; + op.reset(); + op.is_read = true; + + capy::mutable_buffer bufs[max_buffers]; + op.iovec_count = static_cast(param.copy_to(bufs, max_buffers)); + + if (op.iovec_count == 0) + { + *ec = {}; + *bytes_out = 0; + op.cont_op.cont.h = h; + return dispatch_coro(ex, op.cont_op.cont); + } + + for (int i = 0; i < op.iovec_count; ++i) + { + op.iovecs[i].iov_base = bufs[i].data(); + op.iovecs[i].iov_len = bufs[i].size(); + } + + op.h = h; + op.ex = ex; + op.ec_out = ec; + op.bytes_out = bytes_out; + op.start(token); + + op.ex.on_work_started(); + + read_pool_op_.file_ = this; + read_pool_op_.ref_ = this->shared_from_this(); + read_pool_op_.func_ = &posix_stream_file::do_read_work; + if (!svc_.pool().post(&read_pool_op_)) + { + op.impl_ref = std::move(read_pool_op_.ref_); + op.cancelled.store(true, std::memory_order_release); + svc_.post(&read_op_); + } + return std::noop_coroutine(); +} + +inline void +posix_stream_file::do_read_work(pool_work_item* w) noexcept +{ + auto* pw = static_cast(w); + auto* self = pw->file_; + auto& op = self->read_op_; + + if (!op.cancelled.load(std::memory_order_acquire)) + { + ssize_t n; + do + { + n = ::preadv(self->fd_, op.iovecs, op.iovec_count, + static_cast(self->offset_)); + } + while (n < 0 && errno == EINTR); + + if (n >= 0) + { + op.errn = 0; + op.bytes_transferred = static_cast(n); + self->offset_ += static_cast(n); + } + else + { + op.errn = errno; + op.bytes_transferred = 0; + } + } + + op.impl_ref = std::move(pw->ref_); + self->svc_.post(&op); +} + +inline std::coroutine_handle<> +posix_stream_file::write_some( + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param param, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) +{ + auto& op = write_op_; + op.reset(); + op.is_read = false; + + capy::mutable_buffer bufs[max_buffers]; + op.iovec_count = static_cast(param.copy_to(bufs, max_buffers)); + + if (op.iovec_count == 0) + { + *ec = {}; + *bytes_out = 0; + op.cont_op.cont.h = h; + return dispatch_coro(ex, op.cont_op.cont); + } + + for (int i = 0; i < op.iovec_count; ++i) + { + op.iovecs[i].iov_base = bufs[i].data(); + op.iovecs[i].iov_len = bufs[i].size(); + } + + op.h = h; + op.ex = ex; + op.ec_out = ec; + op.bytes_out = bytes_out; + op.start(token); + + op.ex.on_work_started(); + + write_pool_op_.file_ = this; + write_pool_op_.ref_ = this->shared_from_this(); + write_pool_op_.func_ = &posix_stream_file::do_write_work; + if (!svc_.pool().post(&write_pool_op_)) + { + op.impl_ref = std::move(write_pool_op_.ref_); + op.cancelled.store(true, std::memory_order_release); + svc_.post(&write_op_); + } + return std::noop_coroutine(); +} + +inline void +posix_stream_file::do_write_work(pool_work_item* w) noexcept +{ + auto* pw = static_cast(w); + auto* self = pw->file_; + auto& op = self->write_op_; + + if (!op.cancelled.load(std::memory_order_acquire)) + { + ssize_t n; + do + { + n = ::pwritev(self->fd_, op.iovecs, op.iovec_count, + static_cast(self->offset_)); + } + while (n < 0 && errno == EINTR); + + if (n >= 0) + { + op.errn = 0; + op.bytes_transferred = static_cast(n); + self->offset_ += static_cast(n); + } + else + { + op.errn = errno; + op.bytes_transferred = 0; + } + } + + op.impl_ref = std::move(pw->ref_); + self->svc_.post(&op); +} + +} // namespace boost::corosio::detail + +#endif // BOOST_COROSIO_POSIX + +#endif // BOOST_COROSIO_NATIVE_DETAIL_POSIX_POSIX_STREAM_FILE_SERVICE_HPP diff --git a/include/boost/corosio/native/detail/select/select_scheduler.hpp b/include/boost/corosio/native/detail/select/select_scheduler.hpp index 55c25c99f..f818d7e41 100644 --- a/include/boost/corosio/native/detail/select/select_scheduler.hpp +++ b/include/boost/corosio/native/detail/select/select_scheduler.hpp @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include @@ -178,6 +180,8 @@ inline select_scheduler::select_scheduler(capy::execution_context& ctx, int) get_resolver_service(ctx, *this); get_signal_service(ctx, *this); + get_stream_file_service(ctx, *this); + get_random_access_file_service(ctx, *this); completed_ops_.push(&task_op_); } diff --git a/include/boost/corosio/random_access_file.hpp b/include/boost/corosio/random_access_file.hpp new file mode 100644 index 000000000..cd7d6ea7a --- /dev/null +++ b/include/boost/corosio/random_access_file.hpp @@ -0,0 +1,373 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_RANDOM_ACCESS_FILE_HPP +#define BOOST_COROSIO_RANDOM_ACCESS_FILE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include +#include +#include +#include +#include +#include + +namespace boost::corosio { + +/** An asynchronous random-access file for coroutine I/O. + + Provides asynchronous read and write operations at explicit + byte offsets, without maintaining an implicit file position. + + On POSIX platforms, file I/O is dispatched to a thread pool + (blocking `preadv`/`pwritev`) with completion posted back to + the scheduler. On Windows, true overlapped I/O is used via IOCP. + + @par Thread Safety + Distinct objects: Safe.@n + Shared objects: Unsafe. Multiple concurrent reads and writes + are supported from coroutines sharing the same file object, + but external synchronization is required for non-async + operations (open, close, size, resize, etc.). + + @par Example + @code + io_context ioc; + random_access_file f(ioc); + f.open("data.bin", file_base::read_only); + + char buf[4096]; + auto [ec, n] = co_await f.read_some_at( + 0, capy::mutable_buffer(buf, sizeof(buf))); + @endcode +*/ +class BOOST_COROSIO_DECL random_access_file : public io_object +{ +public: + /** Platform-specific random-access file implementation interface. + + Backends derive from this to provide offset-based file I/O. + */ + struct implementation : io_object::implementation + { + /** Initiate a read at the given offset. + + @param offset Byte offset into the file. + @param h Coroutine handle to resume on completion. + @param ex Executor for dispatching the completion. + @param buf The buffer to read into. + @param token Stop token for cancellation. + @param ec Output error code. + @param bytes_out Output bytes transferred. + @return Coroutine handle to resume immediately. + */ + virtual std::coroutine_handle<> read_some_at( + std::uint64_t offset, + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param buf, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) = 0; + + /** Initiate a write at the given offset. + + @param offset Byte offset into the file. + @param h Coroutine handle to resume on completion. + @param ex Executor for dispatching the completion. + @param buf The buffer to write from. + @param token Stop token for cancellation. + @param ec Output error code. + @param bytes_out Output bytes transferred. + @return Coroutine handle to resume immediately. + */ + virtual std::coroutine_handle<> write_some_at( + std::uint64_t offset, + std::coroutine_handle<> h, + capy::executor_ref ex, + buffer_param buf, + std::stop_token token, + std::error_code* ec, + std::size_t* bytes_out) = 0; + + /// Return the platform file descriptor or handle. + virtual native_handle_type native_handle() const noexcept = 0; + + /// Cancel pending asynchronous operations. + virtual void cancel() noexcept = 0; + + /// Return the file size in bytes. + virtual std::uint64_t size() const = 0; + + /// Resize the file to @p new_size bytes. + virtual void resize(std::uint64_t new_size) = 0; + + /// Synchronize file data to stable storage. + virtual void sync_data() = 0; + + /// Synchronize file data and metadata to stable storage. + virtual void sync_all() = 0; + + /// Release ownership of the native handle. + virtual native_handle_type release() = 0; + + /// Adopt an existing native handle. + virtual void assign(native_handle_type handle) = 0; + }; + + /** Awaitable for async read-at operations. */ + template + struct read_some_at_awaitable + { + random_access_file& f_; + std::uint64_t offset_; + MutableBufferSequence buffers_; + std::stop_token token_; + mutable std::error_code ec_; + mutable std::size_t bytes_ = 0; + + read_some_at_awaitable( + random_access_file& f, + std::uint64_t offset, + MutableBufferSequence buffers) + noexcept(std::is_nothrow_move_constructible_v) + : f_(f) + , offset_(offset) + , buffers_(std::move(buffers)) + { + } + + bool await_ready() const noexcept + { + return false; + } + + capy::io_result await_resume() const noexcept + { + return {ec_, bytes_}; + } + + auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + -> std::coroutine_handle<> + { + token_ = env->stop_token; + return f_.get().read_some_at( + offset_, h, env->executor, buffers_, token_, &ec_, &bytes_); + } + }; + + /** Awaitable for async write-at operations. */ + template + struct write_some_at_awaitable + { + random_access_file& f_; + std::uint64_t offset_; + ConstBufferSequence buffers_; + std::stop_token token_; + mutable std::error_code ec_; + mutable std::size_t bytes_ = 0; + + write_some_at_awaitable( + random_access_file& f, + std::uint64_t offset, + ConstBufferSequence buffers) + noexcept(std::is_nothrow_move_constructible_v) + : f_(f) + , offset_(offset) + , buffers_(std::move(buffers)) + { + } + + bool await_ready() const noexcept + { + return false; + } + + capy::io_result await_resume() const noexcept + { + return {ec_, bytes_}; + } + + auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env) + -> std::coroutine_handle<> + { + token_ = env->stop_token; + return f_.get().write_some_at( + offset_, h, env->executor, buffers_, token_, &ec_, &bytes_); + } + }; + +public: + /** Destructor. + + Closes the file if open, cancelling any pending operations. + */ + ~random_access_file() override; + + /** Construct from an execution context. + + @param ctx The execution context that will own this file. + */ + explicit random_access_file(capy::execution_context& ctx); + + /** Construct from an executor. + + @param ex The executor whose context will own this file. + */ + template + requires(!std::same_as, random_access_file>) && + capy::Executor + explicit random_access_file(Ex const& ex) : random_access_file(ex.context()) + { + } + + /** Move constructor. */ + random_access_file(random_access_file&& other) noexcept + : io_object(std::move(other)) + { + } + + /** Move assignment operator. */ + random_access_file& operator=(random_access_file&& other) noexcept + { + if (this != &other) + { + close(); + h_ = std::move(other.h_); + } + return *this; + } + + random_access_file(random_access_file const&) = delete; + random_access_file& operator=(random_access_file const&) = delete; + + /** Open a file. + + @param path The filesystem path to open. + @param mode Bitmask of @ref file_base::flags specifying + access mode and creation behavior. + + @throws std::system_error on failure. + */ + void open( + std::filesystem::path const& path, + file_base::flags mode = file_base::read_only); + + /** Close the file. + + Releases file resources. Any pending operations complete + with `errc::operation_canceled`. + */ + void close(); + + /** Check if the file is open. */ + bool is_open() const noexcept + { +#if BOOST_COROSIO_HAS_IOCP && !defined(BOOST_COROSIO_MRDOCS) + return h_ && get().native_handle() != ~native_handle_type(0); +#else + return h_ && get().native_handle() >= 0; +#endif + } + + /** Read data at the given offset. + + @param offset Byte offset into the file. + @param buffers The buffer sequence to read into. + + @return An awaitable yielding `(error_code, std::size_t)`. + + @throws std::logic_error if the file is not open. + */ + template + auto read_some_at(std::uint64_t offset, MB const& buffers) + { + if (!is_open()) + detail::throw_logic_error("read_some_at: file not open"); + return read_some_at_awaitable(*this, offset, buffers); + } + + /** Write data at the given offset. + + @param offset Byte offset into the file. + @param buffers The buffer sequence to write from. + + @return An awaitable yielding `(error_code, std::size_t)`. + + @throws std::logic_error if the file is not open. + */ + template + auto write_some_at(std::uint64_t offset, CB const& buffers) + { + if (!is_open()) + detail::throw_logic_error("write_some_at: file not open"); + return write_some_at_awaitable(*this, offset, buffers); + } + + /** Cancel pending asynchronous operations. */ + void cancel(); + + /** Get the native file descriptor or handle. */ + native_handle_type native_handle() const noexcept; + + /** Return the file size in bytes. */ + std::uint64_t size() const; + + /** Resize the file. */ + void resize(std::uint64_t new_size); + + /** Synchronize file data to stable storage. */ + void sync_data(); + + /** Synchronize file data and metadata to stable storage. */ + void sync_all(); + + /** Release ownership of the native handle. + + The file object becomes not-open. The caller is + responsible for closing the returned handle. + + @return The native file descriptor or handle. + */ + native_handle_type release(); + + /** Adopt an existing native handle. + + Closes any currently open file before adopting. + The file object takes ownership of the handle. + + @param handle The native file descriptor or handle. + */ + void assign(native_handle_type handle); + +private: + inline implementation& get() const noexcept + { + return *static_cast(h_.get()); + } +}; + +} // namespace boost::corosio + +#endif // BOOST_COROSIO_RANDOM_ACCESS_FILE_HPP diff --git a/include/boost/corosio/stream_file.hpp b/include/boost/corosio/stream_file.hpp new file mode 100644 index 000000000..89f88784a --- /dev/null +++ b/include/boost/corosio/stream_file.hpp @@ -0,0 +1,264 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#ifndef BOOST_COROSIO_STREAM_FILE_HPP +#define BOOST_COROSIO_STREAM_FILE_HPP + +#include +#include +#include +#include +#include +#include +#include +#include + +#include +#include +#include + +namespace boost::corosio { + +/** An asynchronous sequential file for coroutine I/O. + + Provides asynchronous read and write operations on a regular + file with an implicit position that advances after each + operation. + + Inherits from @ref io_stream, so `read_some` and `write_some` + are available and work with any algorithm that accepts an + `io_stream&`. + + On POSIX platforms, file I/O is dispatched to a thread pool + (blocking `preadv`/`pwritev`) with completion posted back to + the scheduler. On Windows, true overlapped I/O is used via IOCP. + + @par Thread Safety + Distinct objects: Safe.@n + Shared objects: Unsafe. Only one asynchronous operation + may be in flight at a time. + + @par Example + @code + io_context ioc; + stream_file f(ioc); + f.open("data.bin", file_base::read_only); + + char buf[4096]; + auto [ec, n] = co_await f.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + if (ec == capy::cond::eof) + // end of file + @endcode +*/ +class BOOST_COROSIO_DECL stream_file : public io_stream +{ +public: + /** Platform-specific file implementation interface. + + Backends derive from this to provide file I/O. + `read_some` and `write_some` are inherited from + @ref io_stream::implementation. + */ + struct implementation : io_stream::implementation + { + /// Return the platform file descriptor or handle. + virtual native_handle_type native_handle() const noexcept = 0; + + /// Cancel pending asynchronous operations. + virtual void cancel() noexcept = 0; + + /// Return the file size in bytes. + virtual std::uint64_t size() const = 0; + + /// Resize the file to @p new_size bytes. + virtual void resize(std::uint64_t new_size) = 0; + + /// Synchronize file data to stable storage. + virtual void sync_data() = 0; + + /// Synchronize file data and metadata to stable storage. + virtual void sync_all() = 0; + + /// Release ownership of the native handle. + virtual native_handle_type release() = 0; + + /// Adopt an existing native handle. + virtual void assign(native_handle_type handle) = 0; + + /** Move the file position. + + @param offset Signed offset from @p origin. + @param origin The reference point for the seek. + @return The new absolute position. + */ + virtual std::uint64_t + seek(std::int64_t offset, file_base::seek_basis origin) = 0; + }; + + /** Destructor. + + Closes the file if open, cancelling any pending operations. + */ + ~stream_file() override; + + /** Construct from an execution context. + + @param ctx The execution context that will own this file. + */ + explicit stream_file(capy::execution_context& ctx); + + /** Construct from an executor. + + @param ex The executor whose context will own this file. + */ + template + requires(!std::same_as, stream_file>) && + capy::Executor + explicit stream_file(Ex const& ex) : stream_file(ex.context()) + { + } + + /** Move constructor. + + Transfers ownership of the file resources. + */ + stream_file(stream_file&& other) noexcept : io_object(std::move(other)) {} + + /** Move assignment operator. + + Closes any existing file and transfers ownership. + */ + stream_file& operator=(stream_file&& other) noexcept + { + if (this != &other) + { + close(); + h_ = std::move(other.h_); + } + return *this; + } + + stream_file(stream_file const&) = delete; + stream_file& operator=(stream_file const&) = delete; + + // read_some() inherited from io_read_stream + // write_some() inherited from io_write_stream + + /** Open a file. + + @param path The filesystem path to open. + @param mode Bitmask of @ref file_base::flags specifying + access mode and creation behavior. + + @throws std::system_error on failure. + */ + void open( + std::filesystem::path const& path, + file_base::flags mode = file_base::read_only); + + /** Close the file. + + Releases file resources. Any pending operations complete + with `errc::operation_canceled`. + */ + void close(); + + /** Check if the file is open. + + @return `true` if the file is open and ready for I/O. + */ + bool is_open() const noexcept + { +#if BOOST_COROSIO_HAS_IOCP && !defined(BOOST_COROSIO_MRDOCS) + return h_ && get().native_handle() != ~native_handle_type(0); +#else + return h_ && get().native_handle() >= 0; +#endif + } + + /** Cancel pending asynchronous operations. + + All outstanding operations complete with + `errc::operation_canceled`. + */ + void cancel(); + + /** Get the native file descriptor or handle. + + @return The native handle, or -1/INVALID_HANDLE_VALUE + if not open. + */ + native_handle_type native_handle() const noexcept; + + /** Return the file size in bytes. + + @throws std::system_error on failure. + */ + std::uint64_t size() const; + + /** Resize the file to @p new_size bytes. + + @param new_size The new file size. + @throws std::system_error on failure. + */ + void resize(std::uint64_t new_size); + + /** Synchronize file data to stable storage. + + @throws std::system_error on failure. + */ + void sync_data(); + + /** Synchronize file data and metadata to stable storage. + + @throws std::system_error on failure. + */ + void sync_all(); + + /** Release ownership of the native handle. + + The file object becomes not-open. The caller is + responsible for closing the returned handle. + + @return The native file descriptor or handle. + */ + native_handle_type release(); + + /** Adopt an existing native handle. + + Closes any currently open file before adopting. + The file object takes ownership of the handle. + + @param handle The native file descriptor or handle. + @throws std::system_error on failure. + */ + void assign(native_handle_type handle); + + /** Move the file position. + + @param offset Signed offset from @p origin. + @param origin The reference point for the seek. + @return The new absolute position. + @throws std::system_error on failure. + */ + std::uint64_t + seek(std::int64_t offset, + file_base::seek_basis origin = file_base::seek_set); + +private: + inline implementation& get() const noexcept + { + return *static_cast(h_.get()); + } +}; + +} // namespace boost::corosio + +#endif // BOOST_COROSIO_STREAM_FILE_HPP diff --git a/src/corosio/src/io_context.cpp b/src/corosio/src/io_context.cpp index badcae8f2..6cee5676e 100644 --- a/src/corosio/src/io_context.cpp +++ b/src/corosio/src/io_context.cpp @@ -39,6 +39,8 @@ #include #include #include +#include +#include #endif namespace boost::corosio { @@ -99,6 +101,8 @@ iocp_t::construct(capy::execution_context& ctx, unsigned concurrency_hint) ctx.make_service(sockets); ctx.make_service(); ctx.make_service(); + ctx.make_service(); + ctx.make_service(); return sched; } diff --git a/src/corosio/src/random_access_file.cpp b/src/corosio/src/random_access_file.cpp new file mode 100644 index 000000000..04e3d1f3b --- /dev/null +++ b/src/corosio/src/random_access_file.cpp @@ -0,0 +1,136 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include +#include +#include + +#if BOOST_COROSIO_HAS_IOCP +#include +#else +#include +#endif + +namespace boost::corosio { + +random_access_file::~random_access_file() +{ + close(); +} + +random_access_file::random_access_file(capy::execution_context& ctx) +#if BOOST_COROSIO_HAS_IOCP + : io_object(create_handle(ctx)) +#else + : io_object(create_handle(ctx)) +#endif +{ +} + +void +random_access_file::open( + std::filesystem::path const& path, file_base::flags mode) +{ + if (is_open()) + close(); + auto& svc = static_cast(h_.service()); + std::error_code ec = svc.open_file(get(), path, mode); + if (ec) + detail::throw_system_error(ec, "random_access_file::open"); +} + +void +random_access_file::close() +{ + if (!is_open()) + return; + h_.service().close(h_); +} + +void +random_access_file::cancel() +{ + if (!is_open()) + return; + get().cancel(); +} + +native_handle_type +random_access_file::native_handle() const noexcept +{ + if (!is_open()) + { +#if BOOST_COROSIO_HAS_IOCP + return static_cast(~0ull); +#else + return -1; +#endif + } + return get().native_handle(); +} + +std::uint64_t +random_access_file::size() const +{ + if (!is_open()) + detail::throw_system_error( + make_error_code(std::errc::bad_file_descriptor), + "random_access_file::size"); + return get().size(); +} + +void +random_access_file::resize(std::uint64_t new_size) +{ + if (!is_open()) + detail::throw_system_error( + make_error_code(std::errc::bad_file_descriptor), + "random_access_file::resize"); + get().resize(new_size); +} + +void +random_access_file::sync_data() +{ + if (!is_open()) + detail::throw_system_error( + make_error_code(std::errc::bad_file_descriptor), + "random_access_file::sync_data"); + get().sync_data(); +} + +void +random_access_file::sync_all() +{ + if (!is_open()) + detail::throw_system_error( + make_error_code(std::errc::bad_file_descriptor), + "random_access_file::sync_all"); + get().sync_all(); +} + +native_handle_type +random_access_file::release() +{ + if (!is_open()) + detail::throw_system_error( + make_error_code(std::errc::bad_file_descriptor), + "random_access_file::release"); + return get().release(); +} + +void +random_access_file::assign(native_handle_type handle) +{ + if (is_open()) + close(); + get().assign(handle); +} + +} // namespace boost::corosio diff --git a/src/corosio/src/stream_file.cpp b/src/corosio/src/stream_file.cpp new file mode 100644 index 000000000..d0003c064 --- /dev/null +++ b/src/corosio/src/stream_file.cpp @@ -0,0 +1,146 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +#include +#include +#include + +#if BOOST_COROSIO_HAS_IOCP +#include +#else +#include +#endif + +namespace boost::corosio { + +stream_file::~stream_file() +{ + close(); +} + +stream_file::stream_file(capy::execution_context& ctx) +#if BOOST_COROSIO_HAS_IOCP + : io_object(create_handle(ctx)) +#else + : io_object(create_handle(ctx)) +#endif +{ +} + +void +stream_file::open( + std::filesystem::path const& path, file_base::flags mode) +{ + if (is_open()) + close(); + auto& svc = static_cast(h_.service()); + std::error_code ec = svc.open_file(get(), path, mode); + if (ec) + detail::throw_system_error(ec, "stream_file::open"); +} + +void +stream_file::close() +{ + if (!is_open()) + return; + h_.service().close(h_); +} + +void +stream_file::cancel() +{ + if (!is_open()) + return; + get().cancel(); +} + +native_handle_type +stream_file::native_handle() const noexcept +{ + if (!is_open()) + { +#if BOOST_COROSIO_HAS_IOCP + return static_cast(~0ull); +#else + return -1; +#endif + } + return get().native_handle(); +} + +std::uint64_t +stream_file::size() const +{ + if (!is_open()) + detail::throw_system_error( + make_error_code(std::errc::bad_file_descriptor), + "stream_file::size"); + return get().size(); +} + +void +stream_file::resize(std::uint64_t new_size) +{ + if (!is_open()) + detail::throw_system_error( + make_error_code(std::errc::bad_file_descriptor), + "stream_file::resize"); + get().resize(new_size); +} + +void +stream_file::sync_data() +{ + if (!is_open()) + detail::throw_system_error( + make_error_code(std::errc::bad_file_descriptor), + "stream_file::sync_data"); + get().sync_data(); +} + +void +stream_file::sync_all() +{ + if (!is_open()) + detail::throw_system_error( + make_error_code(std::errc::bad_file_descriptor), + "stream_file::sync_all"); + get().sync_all(); +} + +native_handle_type +stream_file::release() +{ + if (!is_open()) + detail::throw_system_error( + make_error_code(std::errc::bad_file_descriptor), + "stream_file::release"); + return get().release(); +} + +void +stream_file::assign(native_handle_type handle) +{ + if (is_open()) + close(); + get().assign(handle); +} + +std::uint64_t +stream_file::seek(std::int64_t offset, file_base::seek_basis origin) +{ + if (!is_open()) + detail::throw_system_error( + make_error_code(std::errc::bad_file_descriptor), + "stream_file::seek"); + return get().seek(offset, origin); +} + +} // namespace boost::corosio diff --git a/test/unit/random_access_file.cpp b/test/unit/random_access_file.cpp new file mode 100644 index 000000000..a677cee7d --- /dev/null +++ b/test/unit/random_access_file.cpp @@ -0,0 +1,793 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +// Test that header file is self-contained. +#include + +// GCC emits false-positive "may be used uninitialized" warnings +// for structured bindings with co_await expressions +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif + +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +#if BOOST_COROSIO_POSIX +#include +#include +#else +#include +#endif + +namespace boost::corosio { + +namespace { + +struct temp_file +{ + std::filesystem::path path; + + temp_file(std::string_view prefix = "corosio_raf_test_") + { + path = std::filesystem::temp_directory_path() + / (std::string(prefix) + std::to_string(std::rand())); + } + + temp_file(std::string_view prefix, std::string_view contents) + : temp_file(prefix) + { + std::ofstream ofs(path, std::ios::binary); + ofs.write(contents.data(), static_cast(contents.size())); + } + + ~temp_file() + { + std::error_code ec; + std::filesystem::remove(path, ec); + } + + temp_file(temp_file const&) = delete; + temp_file& operator=(temp_file const&) = delete; +}; + +} // namespace + +struct random_access_file_test +{ + // Construction + + void testConstruction() + { + io_context ioc; + random_access_file f(ioc); + + BOOST_TEST(!f.is_open()); + BOOST_TEST_PASS(); + } + + void testConstructionFromExecutor() + { + io_context ioc; + random_access_file f(ioc.get_executor()); + + BOOST_TEST(!f.is_open()); + BOOST_TEST_PASS(); + } + + void testMoveConstruct() + { + io_context ioc; + random_access_file f1(ioc); + random_access_file f2(std::move(f1)); + + BOOST_TEST_PASS(); + } + + // Open / close + + void testOpenReadOnly() + { + temp_file tmp("raf_open_ro_", "hello"); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, file_base::read_only); + BOOST_TEST(f.is_open()); + + f.close(); + BOOST_TEST(!f.is_open()); + } + + void testOpenNonexistent() + { + io_context ioc; + random_access_file f(ioc); + + bool threw = false; + try + { + f.open("/tmp/corosio_nonexistent_raf_zzz_12345", + file_base::read_only); + } + catch (std::system_error const&) + { + threw = true; + } + BOOST_TEST(threw); + } + + // File metadata + + void testSize() + { + std::string data = "0123456789"; + temp_file tmp("raf_size_", data); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, file_base::read_only); + BOOST_TEST_EQ(f.size(), static_cast(data.size())); + } + + void testResize() + { + temp_file tmp("raf_resize_", "0123456789"); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, file_base::read_write); + f.resize(5); + BOOST_TEST_EQ(f.size(), 5u); + } + + // Async read at offset + + void testReadSomeAt() + { + std::string data = "ABCDEFGHIJ"; + temp_file tmp("raf_read_", data); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, file_base::read_only); + + bool completed = false; + char buf[5] = {}; + + auto task = [](random_access_file& f_ref, char* buf_ptr, + bool& done) -> capy::task<> { + // Read 5 bytes starting at offset 3 + auto [ec, n] = co_await f_ref.read_some_at( + 3, capy::mutable_buffer(buf_ptr, 5)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5u); + done = true; + }; + capy::run_async(ioc.get_executor())(task(f, buf, completed)); + + ioc.run(); + + BOOST_TEST(completed); + BOOST_TEST(std::memcmp(buf, "DEFGH", 5) == 0); + } + + void testReadSomeAtBeginning() + { + std::string data = "hello world"; + temp_file tmp("raf_read0_", data); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, file_base::read_only); + + bool completed = false; + char buf[5] = {}; + + auto task = [](random_access_file& f_ref, char* buf_ptr, + bool& done) -> capy::task<> { + auto [ec, n] = co_await f_ref.read_some_at( + 0, capy::mutable_buffer(buf_ptr, 5)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5u); + done = true; + }; + capy::run_async(ioc.get_executor())(task(f, buf, completed)); + + ioc.run(); + + BOOST_TEST(completed); + BOOST_TEST(std::memcmp(buf, "hello", 5) == 0); + } + + void testReadSomeAtEOF() + { + temp_file tmp("raf_eof_", "hi"); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, file_base::read_only); + + bool got_eof = false; + + auto task = [](random_access_file& f_ref, + bool& eof_out) -> capy::task<> { + char buf[64]; + // Read past end of file + auto [ec, n] = co_await f_ref.read_some_at( + 100, capy::mutable_buffer(buf, sizeof(buf))); + eof_out = (ec == capy::cond::eof); + }; + capy::run_async(ioc.get_executor())(task(f, got_eof)); + + ioc.run(); + + BOOST_TEST(got_eof); + } + + // Async write at offset + + void testWriteSomeAt() + { + temp_file tmp("raf_write_"); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, + file_base::read_write | file_base::create | file_base::truncate); + + bool completed = false; + + auto task = [](random_access_file& f_ref, bool& done) -> capy::task<> { + // Write "hello" at offset 0 + auto [ec, n] = co_await f_ref.write_some_at( + 0, capy::const_buffer("hello", 5)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5u); + done = true; + }; + capy::run_async(ioc.get_executor())(task(f, completed)); + + ioc.run(); + + BOOST_TEST(completed); + + // Verify by reading back + f.close(); + std::ifstream ifs(tmp.path, std::ios::binary); + std::string contents( + (std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + BOOST_TEST_EQ(contents, "hello"); + } + + void testWriteAndReadAtDifferentOffsets() + { + temp_file tmp("raf_wroff_"); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, + file_base::read_write | file_base::create | file_base::truncate); + + bool completed = false; + + auto task = [](random_access_file& f_ref, bool& done) -> capy::task<> { + // Write "AAA" at offset 0 + { + auto [ec, n] = co_await f_ref.write_some_at( + 0, capy::const_buffer("AAA", 3)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 3u); + } + + // Write "BBB" at offset 3 + { + auto [ec, n] = co_await f_ref.write_some_at( + 3, capy::const_buffer("BBB", 3)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 3u); + } + + // Read back from offset 0 + char buf[6] = {}; + { + auto [ec, n] = co_await f_ref.read_some_at( + 0, capy::mutable_buffer(buf, 6)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 6u); + } + BOOST_TEST(std::memcmp(buf, "AAABBB", 6) == 0); + + // Read back from offset 2 (crossing the boundary) + char buf2[4] = {}; + { + auto [ec, n] = co_await f_ref.read_some_at( + 2, capy::mutable_buffer(buf2, 4)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 4u); + } + BOOST_TEST(std::memcmp(buf2, "ABBB", 4) == 0); + + done = true; + }; + capy::run_async(ioc.get_executor())(task(f, completed)); + + ioc.run(); + + BOOST_TEST(completed); + } + + // Sequential operations + + void testSequentialReads() + { + std::string data = "0123456789ABCDEF"; + temp_file tmp("raf_seqrd_", data); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, file_base::read_only); + + int read_count = 0; + + auto task = [](random_access_file& f_ref, + int& count_out) -> capy::task<> { + char buf[4]; + + for (int i = 0; i < 4; ++i) + { + auto [ec, n] = co_await f_ref.read_some_at( + i * 4, capy::mutable_buffer(buf, 4)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 4u); + ++count_out; + } + }; + capy::run_async(ioc.get_executor())(task(f, read_count)); + + ioc.run(); + + BOOST_TEST_EQ(read_count, 4); + } + + // Cancel + + void testCancelNoOperation() + { + temp_file tmp("raf_cancel_", "data"); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, file_base::read_only); + f.cancel(); + + BOOST_TEST_PASS(); + } + + // Sync data + + void testSyncData() + { + temp_file tmp("raf_sync_"); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, + file_base::write_only | file_base::create | file_base::truncate); + + bool completed = false; + + auto task = [](random_access_file& f_ref, + bool& done) -> capy::task<> { + auto [ec, n] = co_await f_ref.write_some_at( + 0, capy::const_buffer("sync", 4)); + BOOST_TEST(!ec); + f_ref.sync_data(); + done = true; + }; + capy::run_async(ioc.get_executor())(task(f, completed)); + + ioc.run(); + + BOOST_TEST(completed); + } + + // Concurrent operations + + void testConcurrentReads() + { + // 4 coroutines reading different offsets of the same file + std::string data = "AAAABBBBCCCCDDDD"; + temp_file tmp("raf_conc_rd_", data); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, file_base::read_only); + + int completed = 0; + + auto reader = [](random_access_file& f_ref, std::uint64_t off, + char expected, int& count) -> capy::task<> { + char buf[4] = {}; + auto [ec, n] = co_await f_ref.read_some_at( + off, capy::mutable_buffer(buf, 4)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 4u); + for (int i = 0; i < 4; ++i) + BOOST_TEST_EQ(buf[i], expected); + ++count; + }; + + // Launch 4 concurrent readers on the same file + capy::run_async(ioc.get_executor())(reader(f, 0, 'A', completed)); + capy::run_async(ioc.get_executor())(reader(f, 4, 'B', completed)); + capy::run_async(ioc.get_executor())(reader(f, 8, 'C', completed)); + capy::run_async(ioc.get_executor())(reader(f, 12, 'D', completed)); + + ioc.run(); + + BOOST_TEST_EQ(completed, 4); + } + + void testConcurrentWrites() + { + // 4 coroutines writing non-overlapping offsets + temp_file tmp("raf_conc_wr_"); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, + file_base::read_write | file_base::create | file_base::truncate); + f.resize(16); + + int completed = 0; + + auto writer = [](random_access_file& f_ref, std::uint64_t off, + char ch, int& count) -> capy::task<> { + char buf[4]; + std::memset(buf, ch, 4); + auto [ec, n] = co_await f_ref.write_some_at( + off, capy::const_buffer(buf, 4)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 4u); + ++count; + }; + + capy::run_async(ioc.get_executor())(writer(f, 0, 'W', completed)); + capy::run_async(ioc.get_executor())(writer(f, 4, 'X', completed)); + capy::run_async(ioc.get_executor())(writer(f, 8, 'Y', completed)); + capy::run_async(ioc.get_executor())(writer(f, 12, 'Z', completed)); + + ioc.run(); + + BOOST_TEST_EQ(completed, 4); + + // Verify file contents + f.close(); + std::ifstream ifs(tmp.path, std::ios::binary); + std::string contents( + (std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + BOOST_TEST_EQ(contents, "WWWWXXXXYYYYZZZZ"); + } + + void testConcurrentReadWrite() + { + // Simultaneous read and write at different offsets + std::string data = "0123456789ABCDEF"; + temp_file tmp("raf_conc_rw_", data); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, file_base::read_write); + + bool read_done = false; + bool write_done = false; + + auto reader = [](random_access_file& f_ref, + bool& done) -> capy::task<> { + char buf[4] = {}; + auto [ec, n] = co_await f_ref.read_some_at( + 0, capy::mutable_buffer(buf, 4)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 4u); + done = true; + }; + + auto writer = [](random_access_file& f_ref, + bool& done) -> capy::task<> { + auto [ec, n] = co_await f_ref.write_some_at( + 12, capy::const_buffer("ZZZZ", 4)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 4u); + done = true; + }; + + capy::run_async(ioc.get_executor())(reader(f, read_done)); + capy::run_async(ioc.get_executor())(writer(f, write_done)); + + ioc.run(); + + BOOST_TEST(read_done); + BOOST_TEST(write_done); + } + + void testManyConcurrentOps() + { + // Stress test: 100 concurrent reads + constexpr int num_ops = 100; + constexpr int block_sz = 4; + std::string data(num_ops * block_sz, 'X'); + for (int i = 0; i < num_ops; ++i) + std::memset(data.data() + i * block_sz, + 'A' + (i % 26), block_sz); + + temp_file tmp("raf_many_", data); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, file_base::read_only); + + std::atomic completed{0}; + + auto reader = [](random_access_file& f_ref, std::uint64_t off, + char expected, std::atomic& count) -> capy::task<> { + char buf[block_sz] = {}; + auto [ec, n] = co_await f_ref.read_some_at( + off, capy::mutable_buffer(buf, block_sz)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, static_cast(block_sz)); + for (int i = 0; i < block_sz; ++i) + BOOST_TEST_EQ(buf[i], expected); + ++count; + }; + + for (int i = 0; i < num_ops; ++i) + { + capy::run_async(ioc.get_executor())( + reader(f, i * block_sz, + static_cast('A' + (i % 26)), completed)); + } + + ioc.run(); + + BOOST_TEST_EQ(completed.load(), num_ops); + } + + void run() + { + testConstruction(); + testConstructionFromExecutor(); + testMoveConstruct(); + + testOpenReadOnly(); + testOpenNonexistent(); + + testSize(); + testResize(); + + testReadSomeAt(); + testReadSomeAtBeginning(); + testReadSomeAtEOF(); + + testWriteSomeAt(); + testWriteAndReadAtDifferentOffsets(); + + testSequentialReads(); + + testCancelNoOperation(); + testSyncData(); + + testConcurrentReads(); + testConcurrentWrites(); + testConcurrentReadWrite(); + testManyConcurrentOps(); + + testSyncAll(); + testRelease(); + testAssign(); + testClosedFileThrows(); + testCancelWithStoppedToken(); + } + + // Operations on closed file + + void testClosedFileThrows() + { + io_context ioc; + random_access_file f(ioc); + BOOST_TEST(!f.is_open()); + + auto expect_throw = [](auto fn) { + bool threw = false; + try { fn(); } + catch (std::system_error const&) { threw = true; } + BOOST_TEST(threw); + }; + + expect_throw([&] { f.size(); }); + expect_throw([&] { f.resize(0); }); + expect_throw([&] { f.sync_data(); }); + expect_throw([&] { f.sync_all(); }); + expect_throw([&] { f.release(); }); + } + + // Cancellation + + void testCancelWithStoppedToken() + { + temp_file tmp("raf_cancel_tok_", "hello world"); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, file_base::read_only); + + std::stop_source stop_src; + stop_src.request_stop(); + + bool completed = false; + std::error_code result_ec; + + auto task = [](random_access_file& f_ref, + std::error_code& ec_out, + bool& done) -> capy::task<> { + char buf[64]; + auto [ec, n] = co_await f_ref.read_some_at( + 0, capy::mutable_buffer(buf, sizeof(buf))); + ec_out = ec; + done = true; + }; + capy::run_async(ioc.get_executor(), stop_src.get_token())( + task(f, result_ec, completed)); + + ioc.run(); + + BOOST_TEST(completed); + BOOST_TEST(result_ec == capy::cond::canceled); + } + + // sync_all + + void testSyncAll() + { + temp_file tmp("raf_syncall_"); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, + file_base::write_only | file_base::create | file_base::truncate); + + bool completed = false; + + auto task = [](random_access_file& f_ref, + bool& done) -> capy::task<> { + auto [ec, n] = co_await f_ref.write_some_at( + 0, capy::const_buffer("sync_all", 8)); + BOOST_TEST(!ec); + f_ref.sync_all(); + done = true; + }; + capy::run_async(ioc.get_executor())(task(f, completed)); + + ioc.run(); + + BOOST_TEST(completed); + } + + // release + + void testRelease() + { + temp_file tmp("raf_release_", "hello"); + io_context ioc; + random_access_file f(ioc); + + f.open(tmp.path, file_base::read_only); + BOOST_TEST(f.is_open()); + + auto handle = f.release(); + BOOST_TEST(!f.is_open()); + + // The handle is still valid — we can read from it + char buf[5] = {}; +#if BOOST_COROSIO_HAS_IOCP + // The released handle is still IOCP-associated, so we must + // set the low-order bit of hEvent to prevent the completion + // from being posted to the (unserviced) IOCP port. + HANDLE h = reinterpret_cast(handle); + HANDLE evt = ::CreateEvent(nullptr, TRUE, FALSE, nullptr); + OVERLAPPED ov{}; + ov.Offset = 0; + ov.hEvent = reinterpret_cast( + reinterpret_cast(evt) | 1); + DWORD bytes_read = 0; + BOOL ok = ::ReadFile(h, buf, 5, &bytes_read, &ov); + if (!ok && ::GetLastError() == ERROR_IO_PENDING) + ok = ::GetOverlappedResult(h, &ov, &bytes_read, TRUE); + BOOST_TEST(ok); + BOOST_TEST_EQ(bytes_read, 5u); + ::CloseHandle(evt); +#else + auto n = ::pread(handle, buf, 5, 0); + BOOST_TEST_EQ(n, 5); +#endif + BOOST_TEST(std::memcmp(buf, "hello", 5) == 0); + +#if BOOST_COROSIO_HAS_IOCP + ::CloseHandle(reinterpret_cast(handle)); +#else + ::close(handle); +#endif + } + + // assign + + void testAssign() + { + temp_file tmp("raf_assign_", "world"); + + // Open with raw platform API, then assign to random_access_file +#if BOOST_COROSIO_HAS_IOCP + HANDLE h = ::CreateFileW( + tmp.path.c_str(), GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED, + nullptr); + BOOST_TEST(h != INVALID_HANDLE_VALUE); + auto raw_handle = reinterpret_cast(h); +#else + int fd = ::open(tmp.path.c_str(), O_RDONLY); + BOOST_TEST(fd >= 0); + auto raw_handle = fd; +#endif + + io_context ioc; + random_access_file f(ioc); + f.assign(raw_handle); + BOOST_TEST(f.is_open()); + + bool completed = false; + + auto task = [](random_access_file& f_ref, + bool& done) -> capy::task<> { + char buf[5] = {}; + auto [ec, n] = co_await f_ref.read_some_at( + 0, capy::mutable_buffer(buf, 5)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5u); + BOOST_TEST(std::memcmp(buf, "world", 5) == 0); + done = true; + }; + capy::run_async(ioc.get_executor())(task(f, completed)); + + ioc.run(); + + BOOST_TEST(completed); + } +}; + +TEST_SUITE(random_access_file_test, "boost.corosio.random_access_file"); + +} // namespace boost::corosio diff --git a/test/unit/stream_file.cpp b/test/unit/stream_file.cpp new file mode 100644 index 000000000..3fdd84604 --- /dev/null +++ b/test/unit/stream_file.cpp @@ -0,0 +1,706 @@ +// +// Copyright (c) 2026 Michael Vandeberg +// +// Distributed under the Boost Software License, Version 1.0. (See accompanying +// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +// +// Official repository: https://github.com/cppalliance/corosio +// + +// Test that header file is self-contained. +#include + +// GCC emits false-positive "may be used uninitialized" warnings +// for structured bindings with co_await expressions +#if defined(__GNUC__) && !defined(__clang__) +#pragma GCC diagnostic ignored "-Wmaybe-uninitialized" +#endif + +#include +#include +#include +#include +#include + +#include "test_suite.hpp" + +#include +#include +#include +#include +#include +#include + +#include + +#if BOOST_COROSIO_POSIX +#include +#include +#else +#include +#endif + +namespace boost::corosio { + +namespace { + +// RAII helper that creates a temp file and removes it on destruction. +struct temp_file +{ + std::filesystem::path path; + + temp_file(std::string_view prefix = "corosio_test_") + { + path = std::filesystem::temp_directory_path() + / (std::string(prefix) + std::to_string(std::rand())); + } + + // Create with initial contents + temp_file(std::string_view prefix, std::string_view contents) + : temp_file(prefix) + { + std::ofstream ofs(path, std::ios::binary); + ofs.write(contents.data(), static_cast(contents.size())); + } + + ~temp_file() + { + std::error_code ec; + std::filesystem::remove(path, ec); + } + + temp_file(temp_file const&) = delete; + temp_file& operator=(temp_file const&) = delete; +}; + +} // namespace + +struct stream_file_test +{ + // Construction and move semantics + + void testConstruction() + { + io_context ioc; + stream_file f(ioc); + + BOOST_TEST(!f.is_open()); + BOOST_TEST_PASS(); + } + + void testConstructionFromExecutor() + { + io_context ioc; + stream_file f(ioc.get_executor()); + + BOOST_TEST(!f.is_open()); + BOOST_TEST_PASS(); + } + + void testMoveConstruct() + { + io_context ioc; + stream_file f1(ioc); + stream_file f2(std::move(f1)); + + BOOST_TEST_PASS(); + } + + void testMoveAssign() + { + io_context ioc; + stream_file f1(ioc); + stream_file f2(ioc); + + f2 = std::move(f1); + BOOST_TEST_PASS(); + } + + // Open / close + + void testOpenReadOnly() + { + temp_file tmp("sf_open_ro_", "hello"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, file_base::read_only); + BOOST_TEST(f.is_open()); + + f.close(); + BOOST_TEST(!f.is_open()); + } + + void testOpenCreateWrite() + { + temp_file tmp("sf_open_cw_"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, file_base::write_only | file_base::create); + BOOST_TEST(f.is_open()); + f.close(); + + // File should exist + BOOST_TEST(std::filesystem::exists(tmp.path)); + } + + void testOpenNonexistent() + { + io_context ioc; + stream_file f(ioc); + + bool threw = false; + try + { + f.open("/tmp/corosio_nonexistent_file_zzz_12345", + file_base::read_only); + } + catch (std::system_error const&) + { + threw = true; + } + BOOST_TEST(threw); + BOOST_TEST(!f.is_open()); + } + + void testOpenExclusive() + { + temp_file tmp("sf_excl_", "existing"); + io_context ioc; + stream_file f(ioc); + + // Opening with create|exclusive on an existing file should fail + bool threw = false; + try + { + f.open(tmp.path, + file_base::write_only | file_base::create + | file_base::exclusive); + } + catch (std::system_error const&) + { + threw = true; + } + BOOST_TEST(threw); + } + + // File metadata + + void testSize() + { + std::string data = "hello world"; + temp_file tmp("sf_size_", data); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, file_base::read_only); + BOOST_TEST_EQ(f.size(), static_cast(data.size())); + } + + void testResize() + { + temp_file tmp("sf_resize_", "hello world"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, file_base::read_write); + f.resize(5); + BOOST_TEST_EQ(f.size(), 5u); + } + + void testSeek() + { + temp_file tmp("sf_seek_", "0123456789"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, file_base::read_only); + + auto pos = f.seek(5, file_base::seek_set); + BOOST_TEST_EQ(pos, 5u); + + pos = f.seek(3, file_base::seek_cur); + BOOST_TEST_EQ(pos, 8u); + + pos = f.seek(-2, file_base::seek_end); + BOOST_TEST_EQ(pos, 8u); // size=10, 10-2=8 + } + + // Async read + + void testReadSome() + { + std::string data = "hello world"; + temp_file tmp("sf_read_", data); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, file_base::read_only); + + bool completed = false; + std::error_code result_ec; + std::size_t result_bytes = 0; + char buf[64] = {}; + + auto task = [](stream_file& f_ref, char* buf_ptr, + std::error_code& ec_out, std::size_t& bytes_out, + bool& done) -> capy::task<> { + auto [ec, n] = co_await f_ref.read_some( + capy::mutable_buffer(buf_ptr, 64)); + ec_out = ec; + bytes_out = n; + done = true; + }; + capy::run_async(ioc.get_executor())( + task(f, buf, result_ec, result_bytes, completed)); + + ioc.run(); + + BOOST_TEST(completed); + BOOST_TEST(!result_ec); + BOOST_TEST_EQ(result_bytes, data.size()); + BOOST_TEST(std::memcmp(buf, data.data(), data.size()) == 0); + } + + void testReadEOF() + { + temp_file tmp("sf_eof_", "hi"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, file_base::read_only); + + bool got_eof = false; + + auto task = [](stream_file& f_ref, bool& eof_out) -> capy::task<> { + char buf[64]; + + // First read: should return 2 bytes + auto [ec1, n1] = co_await f_ref.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + BOOST_TEST(!ec1); + BOOST_TEST_EQ(n1, 2u); + + // Second read: should return EOF + auto [ec2, n2] = co_await f_ref.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + eof_out = (ec2 == capy::cond::eof); + }; + capy::run_async(ioc.get_executor())(task(f, got_eof)); + + ioc.run(); + + BOOST_TEST(got_eof); + } + + // Async write + + void testWriteSome() + { + temp_file tmp("sf_write_"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, + file_base::write_only | file_base::create | file_base::truncate); + + std::string data = "written by corosio"; + bool completed = false; + + auto task = [](stream_file& f_ref, std::string const& data_ref, + bool& done) -> capy::task<> { + auto [ec, n] = co_await f_ref.write_some( + capy::const_buffer(data_ref.data(), data_ref.size())); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, data_ref.size()); + done = true; + }; + capy::run_async(ioc.get_executor())(task(f, data, completed)); + + ioc.run(); + + BOOST_TEST(completed); + + // Verify contents by reading the file back + f.close(); + std::ifstream ifs(tmp.path, std::ios::binary); + std::string contents( + (std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + BOOST_TEST_EQ(contents, data); + } + + // Sequential read/write (verifies position tracking) + + void testSequentialReadWrite() + { + temp_file tmp("sf_seqrw_"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, + file_base::read_write | file_base::create | file_base::truncate); + + bool completed = false; + + auto task = [](stream_file& f_ref, bool& done) -> capy::task<> { + // Write "AAABBB" + { + auto [ec, n] = co_await f_ref.write_some( + capy::const_buffer("AAA", 3)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 3u); + } + { + auto [ec, n] = co_await f_ref.write_some( + capy::const_buffer("BBB", 3)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 3u); + } + + // Seek back to start + f_ref.seek(0, file_base::seek_set); + + // Read back + char buf[6] = {}; + { + auto [ec, n] = co_await f_ref.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 6u); + } + BOOST_TEST(std::memcmp(buf, "AAABBB", 6) == 0); + + done = true; + }; + capy::run_async(ioc.get_executor())(task(f, completed)); + + ioc.run(); + + BOOST_TEST(completed); + } + + // Sync data + + void testSyncData() + { + temp_file tmp("sf_sync_"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, + file_base::write_only | file_base::create | file_base::truncate); + + bool completed = false; + + auto task = [](stream_file& f_ref, bool& done) -> capy::task<> { + auto [ec, n] = co_await f_ref.write_some( + capy::const_buffer("data", 4)); + BOOST_TEST(!ec); + f_ref.sync_data(); + done = true; + }; + capy::run_async(ioc.get_executor())(task(f, completed)); + + ioc.run(); + + BOOST_TEST(completed); + } + + // Cancel + + void testCancelNoOperation() + { + temp_file tmp("sf_cancel_", "data"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, file_base::read_only); + f.cancel(); // Should not crash + + BOOST_TEST_PASS(); + } + + // Open flags + + void testTruncate() + { + temp_file tmp("sf_trunc_", "original data here"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, file_base::write_only | file_base::truncate); + BOOST_TEST_EQ(f.size(), 0u); + } + + // Append mode + + void testAppendMode() + { + temp_file tmp("sf_append_", "hello"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, + file_base::write_only | file_base::append); + + bool completed = false; + + auto task = [](stream_file& f_ref, bool& done) -> capy::task<> { + auto [ec, n] = co_await f_ref.write_some( + capy::const_buffer(" world", 6)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 6u); + done = true; + }; + capy::run_async(ioc.get_executor())(task(f, completed)); + + ioc.run(); + + BOOST_TEST(completed); + + // Verify write went to end, not beginning + f.close(); + std::ifstream ifs(tmp.path, std::ios::binary); + std::string contents( + (std::istreambuf_iterator(ifs)), + std::istreambuf_iterator()); + BOOST_TEST_EQ(contents, "hello world"); + } + + // sync_all + + void testSyncAll() + { + temp_file tmp("sf_syncall_"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, + file_base::write_only | file_base::create | file_base::truncate); + + bool completed = false; + + auto task = [](stream_file& f_ref, bool& done) -> capy::task<> { + auto [ec, n] = co_await f_ref.write_some( + capy::const_buffer("data", 4)); + BOOST_TEST(!ec); + f_ref.sync_all(); + done = true; + }; + capy::run_async(ioc.get_executor())(task(f, completed)); + + ioc.run(); + + BOOST_TEST(completed); + } + + // Release and assign + + void testRelease() + { + temp_file tmp("sf_release_", "hello"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, file_base::read_only); + BOOST_TEST(f.is_open()); + + auto handle = f.release(); + BOOST_TEST(!f.is_open()); + + // The raw handle should still be usable + char buf[5] = {}; +#if BOOST_COROSIO_HAS_IOCP + HANDLE h = reinterpret_cast(handle); + DWORD bytes_read = 0; + OVERLAPPED ov{}; + ov.Offset = 0; + HANDLE evt = ::CreateEvent(nullptr, TRUE, FALSE, nullptr); + ov.hEvent = reinterpret_cast( + reinterpret_cast(evt) | 1); + BOOL ok = ::ReadFile(h, buf, 5, &bytes_read, &ov); + if (!ok && ::GetLastError() == ERROR_IO_PENDING) + ok = ::GetOverlappedResult(h, &ov, &bytes_read, TRUE); + BOOST_TEST(ok); + ::CloseHandle(evt); + ::CloseHandle(h); +#else + auto n = ::pread(handle, buf, 5, 0); + BOOST_TEST_EQ(n, 5); + ::close(handle); +#endif + BOOST_TEST(std::memcmp(buf, "hello", 5) == 0); + } + + void testAssign() + { + temp_file tmp("sf_assign_", "world"); + +#if BOOST_COROSIO_HAS_IOCP + HANDLE h = ::CreateFileW( + tmp.path.c_str(), GENERIC_READ, + FILE_SHARE_READ | FILE_SHARE_WRITE, + nullptr, OPEN_EXISTING, + FILE_ATTRIBUTE_NORMAL | FILE_FLAG_OVERLAPPED + | FILE_FLAG_SEQUENTIAL_SCAN, + nullptr); + BOOST_TEST(h != INVALID_HANDLE_VALUE); + auto raw_handle = reinterpret_cast(h); +#else + int fd = ::open(tmp.path.c_str(), O_RDONLY); + BOOST_TEST(fd >= 0); + auto raw_handle = fd; +#endif + + io_context ioc; + stream_file f(ioc); + f.assign(raw_handle); + BOOST_TEST(f.is_open()); + + bool completed = false; + + auto task = [](stream_file& f_ref, bool& done) -> capy::task<> { + char buf[5] = {}; + auto [ec, n] = co_await f_ref.read_some( + capy::mutable_buffer(buf, 5)); + BOOST_TEST(!ec); + BOOST_TEST_EQ(n, 5u); + BOOST_TEST(std::memcmp(buf, "world", 5) == 0); + done = true; + }; + capy::run_async(ioc.get_executor())(task(f, completed)); + + ioc.run(); + + BOOST_TEST(completed); + } + + // Operations on closed file + + void testClosedFileThrows() + { + io_context ioc; + stream_file f(ioc); + BOOST_TEST(!f.is_open()); + + // Each operation on a closed file should throw + auto expect_throw = [](auto fn) { + bool threw = false; + try { fn(); } + catch (std::system_error const&) { threw = true; } + BOOST_TEST(threw); + }; + + expect_throw([&] { f.size(); }); + expect_throw([&] { f.resize(0); }); + expect_throw([&] { f.sync_data(); }); + expect_throw([&] { f.sync_all(); }); + expect_throw([&] { f.release(); }); + expect_throw([&] { f.seek(0, file_base::seek_set); }); + } + + // Negative seek validation + + void testSeekNegativeThrows() + { + temp_file tmp("sf_seekneg_", "0123456789"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, file_base::read_only); + + // seek_set with negative offset + bool threw = false; + try { f.seek(-1, file_base::seek_set); } + catch (std::system_error const&) { threw = true; } + BOOST_TEST(threw); + + // seek_end past beginning + threw = false; + try { f.seek(-100, file_base::seek_end); } + catch (std::system_error const&) { threw = true; } + BOOST_TEST(threw); + + // seek_cur past beginning + threw = false; + try { f.seek(-100, file_base::seek_cur); } + catch (std::system_error const&) { threw = true; } + BOOST_TEST(threw); + } + + void run() + { + testConstruction(); + testConstructionFromExecutor(); + testMoveConstruct(); + testMoveAssign(); + + testOpenReadOnly(); + testOpenCreateWrite(); + testOpenNonexistent(); + testOpenExclusive(); + + testSize(); + testResize(); + testSeek(); + + testReadSome(); + testReadEOF(); + testWriteSome(); + testSequentialReadWrite(); + + testSyncData(); + testSyncAll(); + testCancelNoOperation(); + testTruncate(); + + testAppendMode(); + testRelease(); + testAssign(); + testClosedFileThrows(); + testSeekNegativeThrows(); + testCancelWithStoppedToken(); + } + + // Cancellation + + void testCancelWithStoppedToken() + { + temp_file tmp("sf_cancel_tok_", "hello world"); + io_context ioc; + stream_file f(ioc); + + f.open(tmp.path, file_base::read_only); + + // Pre-stop the source so the token is already cancelled + // when the coroutine starts + std::stop_source stop_src; + stop_src.request_stop(); + + bool completed = false; + std::error_code result_ec; + + auto task = [](stream_file& f_ref, + std::error_code& ec_out, + bool& done) -> capy::task<> { + char buf[64]; + auto [ec, n] = co_await f_ref.read_some( + capy::mutable_buffer(buf, sizeof(buf))); + ec_out = ec; + done = true; + }; + capy::run_async(ioc.get_executor(), stop_src.get_token())( + task(f, result_ec, completed)); + + ioc.run(); + + BOOST_TEST(completed); + BOOST_TEST(result_ec == capy::cond::canceled); + } +}; + +TEST_SUITE(stream_file_test, "boost.corosio.stream_file"); + +} // namespace boost::corosio