Skip to content

Commit adca257

Browse files
committed
Add async file I/O with concurrent random-access operations
Introduce stream_file and random_access_file for asynchronous file I/O. On POSIX, operations dispatch to a worker thread pool using preadv/pwritev with completion posted back to the scheduler. On Windows, native overlapped I/O is used via IOCP. random_access_file supports unlimited concurrent reads and writes via per-operation heap allocation, matching Asio's concurrency model. stream_file retains embedded single-slot operations since sequential access with an implicit position makes concurrency semantically unclear.
1 parent c49ccf1 commit adca257

27 files changed

+6271
-16
lines changed

doc/modules/ROOT/nav.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
** xref:4.guide/4l.tls.adoc[TLS Encryption]
3535
** xref:4.guide/4m.error-handling.adoc[Error Handling]
3636
** xref:4.guide/4n.buffers.adoc[Buffer Sequences]
37+
** xref:4.guide/4o.file-io.adoc[File I/O]
3738
* xref:5.testing/5.intro.adoc[Testing]
3839
** xref:5.testing/5a.mocket.adoc[Mock Sockets]
3940
* xref:benchmark-report.adoc[Benchmarks]
Lines changed: 203 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,203 @@
1+
//
2+
// Copyright (c) 2026 Michael Vandeberg
3+
//
4+
// Distributed under the Boost Software License, Version 1.0. (See accompanying
5+
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6+
//
7+
// Official repository: https://github.com/cppalliance/corosio
8+
//
9+
10+
= File I/O
11+
12+
Corosio provides two classes for asynchronous file operations:
13+
`stream_file` for sequential access and `random_access_file` for
14+
offset-based access. Both dispatch I/O to a worker thread on POSIX
15+
platforms and use native overlapped I/O on Windows.
16+
17+
[NOTE]
18+
====
19+
Code snippets assume:
20+
[source,cpp]
21+
----
22+
#include <boost/corosio/stream_file.hpp>
23+
#include <boost/corosio/random_access_file.hpp>
24+
namespace corosio = boost::corosio;
25+
----
26+
====
27+
28+
== Stream File
29+
30+
`stream_file` reads and writes sequentially, maintaining an internal
31+
position that advances after each operation. It inherits from `io_stream`,
32+
so it works with any algorithm that accepts an `io_stream&`.
33+
34+
=== Reading a File
35+
36+
[source,cpp]
37+
----
38+
corosio::stream_file f(ioc);
39+
f.open("data.bin", corosio::file_base::read_only);
40+
41+
char buf[4096];
42+
auto [ec, n] = co_await f.read_some(
43+
capy::mutable_buffer(buf, sizeof(buf)));
44+
45+
if (ec == capy::cond::eof)
46+
// reached end of file
47+
----
48+
49+
=== Writing a File
50+
51+
[source,cpp]
52+
----
53+
corosio::stream_file f(ioc);
54+
f.open("output.bin",
55+
corosio::file_base::write_only
56+
| corosio::file_base::create
57+
| corosio::file_base::truncate);
58+
59+
std::string data = "hello world";
60+
auto [ec, n] = co_await f.write_some(
61+
capy::const_buffer(data.data(), data.size()));
62+
----
63+
64+
=== Seeking
65+
66+
The file position can be moved with `seek()`:
67+
68+
[source,cpp]
69+
----
70+
f.seek(0, corosio::file_base::seek_set); // beginning
71+
f.seek(100, corosio::file_base::seek_cur); // forward 100 bytes
72+
f.seek(-10, corosio::file_base::seek_end); // 10 bytes before end
73+
----
74+
75+
== Random Access File
76+
77+
`random_access_file` reads and writes at explicit byte offsets
78+
without maintaining an internal position. This is useful for
79+
databases, indices, or any workload that accesses non-sequential
80+
regions of a file.
81+
82+
=== Reading at an Offset
83+
84+
[source,cpp]
85+
----
86+
corosio::random_access_file f(ioc);
87+
f.open("data.bin", corosio::file_base::read_only);
88+
89+
char buf[256];
90+
auto [ec, n] = co_await f.read_some_at(
91+
1024, // byte offset
92+
capy::mutable_buffer(buf, sizeof(buf)));
93+
----
94+
95+
=== Writing at an Offset
96+
97+
[source,cpp]
98+
----
99+
corosio::random_access_file f(ioc);
100+
f.open("data.bin", corosio::file_base::read_write);
101+
102+
auto [ec, n] = co_await f.write_some_at(
103+
512, capy::const_buffer("patched", 7));
104+
----
105+
106+
== Open Flags
107+
108+
Both file types accept a bitmask of `file_base::flags` when opening:
109+
110+
[cols="1,3"]
111+
|===
112+
| Flag | Meaning
113+
114+
| `read_only` | Open for reading (default)
115+
| `write_only` | Open for writing
116+
| `read_write` | Open for both reading and writing
117+
| `create` | Create the file if it does not exist
118+
| `exclusive` | Fail if the file already exists (requires `create`)
119+
| `truncate` | Truncate the file to zero length on open
120+
| `append` | Seek to end on open (stream_file only)
121+
| `sync_all_on_write` | Synchronize data to disk on each write
122+
|===
123+
124+
Flags are combined with `|`:
125+
126+
[source,cpp]
127+
----
128+
f.open("log.txt",
129+
corosio::file_base::write_only
130+
| corosio::file_base::create
131+
| corosio::file_base::append);
132+
----
133+
134+
== File Metadata
135+
136+
Both file types provide synchronous metadata operations:
137+
138+
[source,cpp]
139+
----
140+
auto bytes = f.size(); // file size in bytes
141+
f.resize(1024); // truncate or extend
142+
f.sync_data(); // flush data to stable storage
143+
f.sync_all(); // flush data and metadata
144+
----
145+
146+
`stream_file` additionally provides `seek()` for repositioning.
147+
148+
== Native Handle Access
149+
150+
Both file types support adopting and releasing native handles:
151+
152+
[source,cpp]
153+
----
154+
// Release ownership — caller must close the handle
155+
auto handle = f.release();
156+
assert(!f.is_open());
157+
158+
// Adopt an existing handle — file takes ownership
159+
corosio::random_access_file f2(ioc);
160+
f2.assign(handle);
161+
----
162+
163+
== Error Handling
164+
165+
File operations follow the same error model as sockets. Reads past
166+
end-of-file return `capy::cond::eof`:
167+
168+
[source,cpp]
169+
----
170+
auto [ec, n] = co_await f.read_some(buf);
171+
if (ec == capy::cond::eof)
172+
{
173+
// no more data
174+
}
175+
else if (ec)
176+
{
177+
// I/O error
178+
}
179+
----
180+
181+
Opening a nonexistent file with `read_only` throws `std::system_error`.
182+
Use `create` to create files that may not exist.
183+
184+
== Thread Safety
185+
186+
* Distinct objects are safe to use concurrently.
187+
* `random_access_file` supports multiple concurrent reads and writes
188+
from coroutines sharing the same file object. Each operation is
189+
independently heap-allocated.
190+
* `stream_file` allows at most one asynchronous operation in flight at
191+
a time (same as Asio's stream_file). Sequential access with an
192+
implicit position makes concurrent ops semantically undefined.
193+
* Non-async operations (open, close, size, resize, etc.) require
194+
external synchronization.
195+
196+
== Platform Notes
197+
198+
On Linux, macOS, and BSD, file I/O is dispatched to a shared worker
199+
thread pool using `preadv`/`pwritev`. This is the same pool used by
200+
the resolver.
201+
202+
On Windows, file I/O uses native IOCP overlapped I/O via
203+
`ReadFile`/`WriteFile` with `FILE_FLAG_OVERLAPPED`.

include/boost/corosio.hpp

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,13 +13,16 @@
1313
#include <boost/corosio/backend.hpp>
1414
#include <boost/corosio/cancel.hpp>
1515
#include <boost/corosio/endpoint.hpp>
16+
#include <boost/corosio/file_base.hpp>
1617
#include <boost/corosio/io_context.hpp>
1718
#include <boost/corosio/ipv4_address.hpp>
1819
#include <boost/corosio/ipv6_address.hpp>
20+
#include <boost/corosio/random_access_file.hpp>
1921
#include <boost/corosio/resolver.hpp>
2022
#include <boost/corosio/resolver_results.hpp>
2123
#include <boost/corosio/signal_set.hpp>
2224
#include <boost/corosio/socket_option.hpp>
25+
#include <boost/corosio/stream_file.hpp>
2326
#include <boost/corosio/tcp_acceptor.hpp>
2427
#include <boost/corosio/tcp_server.hpp>
2528
#include <boost/corosio/tcp_socket.hpp>

include/boost/corosio/backend.hpp

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -176,6 +176,11 @@ class win_signals;
176176
class win_resolver;
177177
class win_resolver_service;
178178

179+
class win_stream_file;
180+
class win_file_service;
181+
class win_random_access_file;
182+
class win_random_access_file_service;
183+
179184
} // namespace detail
180185

181186
/// Backend tag for the Windows I/O Completion Ports multiplexer.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
//
2+
// Copyright (c) 2026 Michael Vandeberg
3+
//
4+
// Distributed under the Boost Software License, Version 1.0. (See accompanying
5+
// file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt)
6+
//
7+
// Official repository: https://github.com/cppalliance/corosio
8+
//
9+
10+
#ifndef BOOST_COROSIO_DETAIL_FILE_SERVICE_HPP
11+
#define BOOST_COROSIO_DETAIL_FILE_SERVICE_HPP
12+
13+
#include <boost/corosio/detail/config.hpp>
14+
#include <boost/corosio/stream_file.hpp>
15+
#include <boost/capy/ex/execution_context.hpp>
16+
17+
#include <filesystem>
18+
#include <system_error>
19+
20+
namespace boost::corosio::detail {
21+
22+
/** Abstract stream file service base class.
23+
24+
Concrete implementations (posix, IOCP) inherit from
25+
this class and provide platform-specific file operations.
26+
The context constructor installs whichever backend via
27+
`make_service`, and `stream_file.cpp` retrieves it via
28+
`use_service<file_service>()`.
29+
*/
30+
class BOOST_COROSIO_DECL file_service
31+
: public capy::execution_context::service
32+
, public io_object::io_service
33+
{
34+
public:
35+
/// Identifies this service for `execution_context` lookup.
36+
using key_type = file_service;
37+
38+
/** Open a file.
39+
40+
Opens the file at the given path with the specified flags
41+
and associates it with the platform I/O mechanism.
42+
43+
@param impl The file implementation to initialize.
44+
@param path The filesystem path to open.
45+
@param mode Bitmask of file_base::flags.
46+
@return Error code on failure, empty on success.
47+
*/
48+
virtual std::error_code open_file(
49+
stream_file::implementation& impl,
50+
std::filesystem::path const& path,
51+
file_base::flags mode) = 0;
52+
53+
protected:
54+
file_service() = default;
55+
~file_service() override = default;
56+
};
57+
58+
} // namespace boost::corosio::detail
59+
60+
#endif // BOOST_COROSIO_DETAIL_FILE_SERVICE_HPP

include/boost/corosio/detail/intrusive.hpp

Lines changed: 32 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -65,10 +65,11 @@ class intrusive_list
6565

6666
void push_back(T* w) noexcept
6767
{
68-
w->next_ = nullptr;
69-
w->prev_ = tail_;
68+
auto* n = static_cast<node*>(w);
69+
n->next_ = nullptr;
70+
n->prev_ = tail_;
7071
if (tail_)
71-
tail_->next_ = w;
72+
static_cast<node*>(tail_)->next_ = w;
7273
else
7374
head_ = w;
7475
tail_ = w;
@@ -80,9 +81,9 @@ class intrusive_list
8081
return;
8182
if (tail_)
8283
{
83-
tail_->next_ = other.head_;
84-
other.head_->prev_ = tail_;
85-
tail_ = other.tail_;
84+
static_cast<node*>(tail_)->next_ = other.head_;
85+
static_cast<node*>(other.head_)->prev_ = tail_;
86+
tail_ = other.tail_;
8687
}
8788
else
8889
{
@@ -98,28 +99,43 @@ class intrusive_list
9899
if (!head_)
99100
return nullptr;
100101
T* w = head_;
101-
head_ = head_->next_;
102+
head_ = static_cast<node*>(head_)->next_;
102103
if (head_)
103-
head_->prev_ = nullptr;
104+
static_cast<node*>(head_)->prev_ = nullptr;
104105
else
105106
tail_ = nullptr;
106107
// Defensive: clear stale linkage so remove() on a
107108
// popped node cannot corrupt the list.
108-
w->next_ = nullptr;
109-
w->prev_ = nullptr;
109+
auto* n = static_cast<node*>(w);
110+
n->next_ = nullptr;
111+
n->prev_ = nullptr;
110112
return w;
111113
}
112114

113115
void remove(T* w) noexcept
114116
{
115-
if (w->prev_)
116-
w->prev_->next_ = w->next_;
117+
auto* n = static_cast<node*>(w);
118+
// Already detached — nothing to do.
119+
if (!n->next_ && !n->prev_ && head_ != w && tail_ != w)
120+
return;
121+
if (n->prev_)
122+
static_cast<node*>(n->prev_)->next_ = n->next_;
117123
else
118-
head_ = w->next_;
119-
if (w->next_)
120-
w->next_->prev_ = w->prev_;
124+
head_ = n->next_;
125+
if (n->next_)
126+
static_cast<node*>(n->next_)->prev_ = n->prev_;
121127
else
122-
tail_ = w->prev_;
128+
tail_ = n->prev_;
129+
n->next_ = nullptr;
130+
n->prev_ = nullptr;
131+
}
132+
133+
/// Invoke @p f for each element in the list.
134+
template<class F>
135+
void for_each(F f)
136+
{
137+
for (T* p = head_; p; p = static_cast<node*>(p)->next_)
138+
f(p);
123139
}
124140
};
125141

0 commit comments

Comments
 (0)