include/boost/corosio/random_access_file.hpp

95.7% Lines (45/47) 100.0% List of functions (17/17) 71.4% Branches (10/14)
random_access_file.hpp
f(x) Functions (17)
Function Calls Lines Branches Blocks
boost::corosio::random_access_file::implementation::implementation() :73 222x 100.0% – 100.0% boost::corosio::random_access_file::implementation::~implementation() :73 222x 100.0% – 100.0% boost::corosio::random_access_file::read_some_at_awaitable<boost::capy::mutable_buffer>::read_some_at_awaitable(boost::corosio::random_access_file::read_some_at_awaitable<boost::capy::mutable_buffer>&&) :161 1372x 100.0% – 100.0% boost::corosio::random_access_file::read_some_at_awaitable<boost::capy::mutable_buffer>::~read_some_at_awaitable() :161 2058x 100.0% – 100.0% boost::corosio::random_access_file::read_some_at_awaitable<boost::capy::mutable_buffer>::read_some_at_awaitable(boost::corosio::random_access_file&, unsigned long long, boost::capy::mutable_buffer) :174 686x 100.0% – 100.0% boost::corosio::random_access_file::read_some_at_awaitable<boost::capy::mutable_buffer>::dispatch(std::__1::coroutine_handle<void>, boost::capy::executor_ref) const :188 337x 83.3% 50.0% 50.0% boost::corosio::random_access_file::write_some_at_awaitable<boost::capy::const_buffer>::write_some_at_awaitable(boost::corosio::random_access_file::write_some_at_awaitable<boost::capy::const_buffer>&&) :202 364x 100.0% – 100.0% boost::corosio::random_access_file::write_some_at_awaitable<boost::capy::const_buffer>::~write_some_at_awaitable() :202 546x 100.0% – 100.0% boost::corosio::random_access_file::write_some_at_awaitable<boost::capy::const_buffer>::write_some_at_awaitable(boost::corosio::random_access_file&, unsigned long long, boost::capy::const_buffer) :215 182x 100.0% – 100.0% boost::corosio::random_access_file::write_some_at_awaitable<boost::capy::const_buffer>::dispatch(std::__1::coroutine_handle<void>, boost::capy::executor_ref) const :229 87x 83.3% 50.0% 50.0% boost::corosio::random_access_file::random_access_file<boost::corosio::io_context::executor_type>(boost::corosio::io_context::executor_type const&) :258 2x 100.0% – 100.0% boost::corosio::random_access_file::random_access_file(boost::corosio::random_access_file&&) :263 4x 100.0% – 100.0% boost::corosio::random_access_file::is_open() const :314 1080x 100.0% 100.0% 100.0% auto boost::corosio::random_access_file::read_some_at<boost::capy::mutable_buffer>(unsigned long long, boost::capy::mutable_buffer const&) :333 343x 100.0% 100.0% 100.0% auto boost::corosio::random_access_file::write_some_at<boost::capy::const_buffer>(unsigned long long, boost::capy::const_buffer const&) :351 91x 100.0% 100.0% 100.0% boost::corosio::random_access_file::random_access_file(boost::corosio::io_object::handle) :472 16x 100.0% – 100.0% boost::corosio::random_access_file::get() const :475 1751x 100.0% – 100.0%
Line Branch TLA Hits Source Code
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_RANDOM_ACCESS_FILE_HPP
11 #define BOOST_COROSIO_RANDOM_ACCESS_FILE_HPP
12
13 #include <boost/corosio/detail/config.hpp>
14 #include <boost/corosio/detail/platform.hpp>
15 #include <boost/corosio/detail/except.hpp>
16 #include <boost/corosio/detail/native_handle.hpp>
17 #include <boost/corosio/detail/buffer_param.hpp>
18 #include <boost/corosio/detail/op_base.hpp>
19 #include <boost/corosio/error.hpp>
20 #include <boost/corosio/file_base.hpp>
21 #include <boost/corosio/io/io_object.hpp>
22 #include <boost/capy/continuation.hpp>
23 #include <boost/capy/io_result.hpp>
24 #include <boost/capy/ex/executor_ref.hpp>
25 #include <boost/capy/ex/execution_context.hpp>
26 #include <boost/capy/ex/io_env.hpp>
27 #include <boost/capy/concept/executor.hpp>
28 #include <boost/capy/buffers.hpp>
29
30 #include <concepts>
31 #include <coroutine>
32 #include <cstddef>
33 #include <cstdint>
34 #include <type_traits>
35 #include <filesystem>
36 #include <stop_token>
37 #include <system_error>
38
39 namespace boost::corosio {
40
41 /** Reads and writes a file at arbitrary offsets, from a coroutine.
42
43 Provides asynchronous read and write operations at explicit
44 byte offsets, without maintaining an implicit file position.
45
46 On POSIX platforms, file I/O is dispatched to a thread pool
47 (blocking `preadv`/`pwritev`) with completion posted back to
48 the scheduler. On Windows, true overlapped I/O is used via IOCP.
49
50 On Windows, while the file is open, its handle is bound to the
51 execution context's completion port. Every overlapped call on the
52 handle queues a packet to that port. Do not issue your own
53 overlapped I/O on `native_handle()` (`DeviceIoControl`,
54 `ReadFile`) unless the `OVERLAPPED`'s `hEvent` has its low-order
55 bit set, which suppresses the packet.
56
57 @par Thread Safety
58 Distinct objects: Safe.@n
59 Shared objects: Unsafe. Coroutines sharing the same file object may
60 run multiple concurrent reads and writes. Non-async operations such as open, close, size, and resize require external synchronization.
61
62 @par Example
63 @par !example random_access_file
64 */
65 class BOOST_COROSIO_DECL random_access_file : public io_object
66 {
67 public:
68 /** Declares the offset-based file operations a platform backend
69 must implement.
70
71 Backends derive from this to provide offset-based file I/O.
72 */
73 struct implementation : io_object::implementation
74 {
75 /** Initiate a read at the given offset.
76
77 @param offset Byte offset into the file.
78 @param cont The awaiting coroutine's continuation. It must
79 stay valid until `cont.h` is resumed through @p ex.
80 @param ex Executor for dispatching the completion.
81 @param buf The buffer to read into.
82 @param token Stop token for cancellation.
83 @param ec Output error code.
84 @param bytes_out Output bytes transferred.
85 @return Coroutine handle to resume immediately.
86 */
87 virtual std::coroutine_handle<> read_some_at(
88 std::uint64_t offset,
89 capy::continuation& cont,
90 capy::executor_ref ex,
91 buffer_param buf,
92 std::stop_token token,
93 std::error_code* ec,
94 std::size_t* bytes_out) = 0;
95
96 /** Initiate a write at the given offset.
97
98 @param offset Byte offset into the file.
99 @param cont The awaiting coroutine's continuation. It must
100 stay valid until `cont.h` is resumed through @p ex.
101 @param ex Executor for dispatching the completion.
102 @param buf The buffer to write from.
103 @param token Stop token for cancellation.
104 @param ec Output error code.
105 @param bytes_out Output bytes transferred.
106 @return Coroutine handle to resume immediately.
107 */
108 virtual std::coroutine_handle<> write_some_at(
109 std::uint64_t offset,
110 capy::continuation& cont,
111 capy::executor_ref ex,
112 buffer_param buf,
113 std::stop_token token,
114 std::error_code* ec,
115 std::size_t* bytes_out) = 0;
116
117 /// Return the platform file descriptor or handle.
118 virtual native_handle_type native_handle() const noexcept = 0;
119
120 /// Cancel pending asynchronous operations.
121 virtual void cancel() noexcept = 0;
122
123 /// Return the file size in bytes.
124 virtual std::uint64_t size() const = 0;
125
126 /** Resize the file to @p new_size bytes.
127
128 @param new_size The requested size in bytes.
129
130 @return The error code, empty on success.
131 */
132 virtual std::error_code resize(std::uint64_t new_size) noexcept = 0;
133
134 /** Synchronize file data to stable storage.
135
136 @return The error code, empty on success.
137 */
138 virtual std::error_code sync_data() noexcept = 0;
139
140 /** Synchronize file data and metadata to stable storage.
141
142 @return The error code, empty on success.
143 */
144 virtual std::error_code sync_all() noexcept = 0;
145
146 /// Release ownership of the native handle.
147 virtual native_handle_type release() = 0;
148
149 /** Adopt an existing native handle.
150
151 @param handle The native handle to adopt. The implementation takes
152 ownership and closes it.
153
154 @return The error code, empty on success.
155 */
156 virtual std::error_code assign(native_handle_type handle) noexcept = 0;
157 };
158
159 /** Awaitable for async read-at operations. */
160 template<class MutableBufferSequence>
161 struct read_some_at_awaitable
162 : detail::bytes_op_base<read_some_at_awaitable<MutableBufferSequence>>
163 {
164 private:
165 friend random_access_file;
166 friend detail::bytes_op_base<
167 read_some_at_awaitable<MutableBufferSequence>>;
168
169 random_access_file& f_;
170 std::uint64_t offset_;
171 MutableBufferSequence buffers_;
172 mutable capy::continuation cont_;
173
174 1029x read_some_at_awaitable(
175 random_access_file& f,
176 std::uint64_t offset,
177 MutableBufferSequence
178 buffers) noexcept(std::
179 is_nothrow_move_constructible_v<
180 MutableBufferSequence>)
181 343x : f_(f)
182 343x , offset_(offset)
183 343x , buffers_(std::move(buffers))
184 343x {
185 686x }
186
187 std::coroutine_handle<>
188 337x dispatch(std::coroutine_handle<> h, capy::executor_ref ex) const
189 {
190 // The continuation lives in the awaiting frame, which stays
191 // put until resumption -- unlike the per-call op, which is
192 // freed before the coroutine runs.
193 337x cont_.h = h;
194
2/4
✓ Branch 0 taken 337 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 337 times.
✗ Branch 3 not taken.
674x return f_.get().read_some_at(
195 337x offset_, cont_, ex, buffers_, this->token_, &this->ec_,
196 337x &this->bytes_);
197 ✗ }
198 };
199
200 /** Awaitable for async write-at operations. */
201 template<class ConstBufferSequence>
202 struct write_some_at_awaitable
203 : detail::bytes_op_base<write_some_at_awaitable<ConstBufferSequence>>
204 {
205 private:
206 friend random_access_file;
207 friend detail::bytes_op_base<
208 write_some_at_awaitable<ConstBufferSequence>>;
209
210 random_access_file& f_;
211 std::uint64_t offset_;
212 ConstBufferSequence buffers_;
213 mutable capy::continuation cont_;
214
215 273x write_some_at_awaitable(
216 random_access_file& f,
217 std::uint64_t offset,
218 ConstBufferSequence
219 buffers) noexcept(std::
220 is_nothrow_move_constructible_v<
221 ConstBufferSequence>)
222 91x : f_(f)
223 91x , offset_(offset)
224 91x , buffers_(std::move(buffers))
225 91x {
226 182x }
227
228 std::coroutine_handle<>
229 87x dispatch(std::coroutine_handle<> h, capy::executor_ref ex) const
230 {
231 87x cont_.h = h;
232
2/4
✓ Branch 0 taken 87 times.
✗ Branch 1 not taken.
✓ Branch 2 taken 87 times.
✗ Branch 3 not taken.
174x return f_.get().write_some_at(
233 87x offset_, cont_, ex, buffers_, this->token_, &this->ec_,
234 87x &this->bytes_);
235 ✗ }
236 };
237
238 public:
239 /** Destructor.
240
241 Closes the file if open, cancelling any pending operations.
242 */
243 ~random_access_file() override;
244
245 /** Construct from an execution context.
246
247 @param ctx The execution context that owns this file.
248 */
249 explicit random_access_file(capy::execution_context& ctx);
250
251 /** Construct from an executor.
252
253 @param ex The executor whose context owns this file.
254 */
255 template<class Ex>
256 requires(!std::same_as<std::remove_cvref_t<Ex>, random_access_file>) &&
257 capy::Executor<Ex>
258 2x explicit random_access_file(Ex const& ex) : random_access_file(ex.context())
259 {
260 2x }
261
262 /** Move constructor. */
263 4x random_access_file(random_access_file&& other) noexcept
264 2x : io_object(std::move(other))
265 4x {
266 4x }
267
268 /** Move assignment operator. */
269 random_access_file& operator=(random_access_file&& other) noexcept
270 {
271 if (this != &other)
272 {
273 close();
274 h_ = std::move(other.h_);
275 }
276 return *this;
277 }
278
279 /// Copy construction is disabled; the handle is uniquely owned.
280 random_access_file(random_access_file const&) = delete;
281 /// Copy assignment is disabled; the handle is uniquely owned.
282 random_access_file& operator=(random_access_file const&) = delete;
283
284 /** Open a file.
285
286 Failures such as a missing file or insufficient permissions
287 are expected runtime conditions and are reported through the
288 returned error code. If the file is already open, it is
289 closed first.
290
291 @param path The filesystem path to open.
292 @param mode Bitmask of @ref file_base::flags specifying
293 access mode and creation behavior.
294
295 @return The error code, empty on success.
296 */
297 [[nodiscard]] std::error_code open(
298 std::filesystem::path const& path,
299 file_base::flags mode = file_base::read_only) noexcept;
300
301 /** Close the file.
302
303 Releases file resources. Pending operations complete through the
304 same path as @ref cancel: one still in flight completes with
305 `errc::operation_canceled`. An operation whose result is already
306 decided reports that result.
307 */
308 void close() noexcept;
309
310 /** Check if the file is open.
311
312 @return `true` if the file holds an open handle.
313 */
314 1080x bool is_open() const noexcept
315 {
316 #if BOOST_COROSIO_HAS_IOCP && !defined(BOOST_COROSIO_MRDOCS)
317 return h_ && get().native_handle() != ~native_handle_type(0);
318 #else
319
2/2
✓ Branch 0 taken 2 times.
✓ Branch 1 taken 1054 times.
1080x return h_ && get().native_handle() >= 0;
320 #endif
321 }
322
323 /** Read data at the given offset.
324
325 @param offset Byte offset into the file.
326 @param buffers The buffer sequence to read into.
327
328 @return An awaitable yielding `(error_code, std::size_t)`.
329
330 A closed file reports `errc::bad_file_descriptor`.
331 */
332 template<capy::MutableBufferSequence MB>
333 343x [[nodiscard]] auto read_some_at(std::uint64_t offset, MB const& buffers)
334 {
335 343x read_some_at_awaitable<MB> aw(*this, offset, buffers);
336
2/2
✓ Branch 0 taken 337 times.
✓ Branch 1 taken 2 times.
343x if (!is_open())
337 2x aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
338 343x return aw;
339 343x }
340
341 /** Write data at the given offset.
342
343 @param offset Byte offset into the file.
344 @param buffers The buffer sequence to write from.
345
346 @return An awaitable yielding `(error_code, std::size_t)`.
347
348 A closed file reports `errc::bad_file_descriptor`.
349 */
350 template<capy::ConstBufferSequence CB>
351 91x [[nodiscard]] auto write_some_at(std::uint64_t offset, CB const& buffers)
352 {
353 91x write_some_at_awaitable<CB> aw(*this, offset, buffers);
354
2/2
✓ Branch 0 taken 85 times.
✓ Branch 1 taken 2 times.
91x if (!is_open())
355 2x aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
356 91x return aw;
357 91x }
358
359 /** Cancel pending asynchronous operations. */
360 void cancel() noexcept;
361
362 /** Get the native file descriptor or handle. */
363 native_handle_type native_handle() const noexcept;
364
365 /** Return the file size in bytes.
366
367 @return The current size of the file, in bytes.
368
369 @throws std::system_error If the file is not open, or if the
370 underlying size query fails.
371 */
372 std::uint64_t size() const;
373
374 /** Resize the file to @p new_size bytes.
375
376 Failures such as insufficient disk space are reported
377 through the returned error code. A closed file reports
378 `errc::bad_file_descriptor`.
379
380 @param new_size The new file size.
381
382 @return The error code, empty on success.
383 */
384 [[nodiscard]] std::error_code resize(std::uint64_t new_size) noexcept;
385
386 /** Synchronize file data to stable storage.
387
388 Write-back failures such as device I/O errors surface here
389 and are reported through the returned error code. A closed
390 file reports `errc::bad_file_descriptor`.
391
392 @return The error code, empty on success.
393 */
394 [[nodiscard]] std::error_code sync_data() noexcept;
395
396 /** Synchronize file data and metadata to stable storage.
397
398 Write-back failures such as device I/O errors surface here
399 and are reported through the returned error code. A closed
400 file reports `errc::bad_file_descriptor`.
401
402 @return The error code, empty on success.
403 */
404 [[nodiscard]] std::error_code sync_all() noexcept;
405
406 /** Release ownership of the native handle.
407
408 The file object becomes not-open. The caller is
409 responsible for closing the returned handle.
410
411 `release()` cancels pending operations first. On Windows, the
412 object keeps the handle and this throws if one is still in
413 flight. It does the same if Windows refuses to detach the
414 handle from the execution context's completion port. Call
415 `release()` again once the cancelled operations have
416 completed. Detaching requires Windows 8.1 or later.
417
418 @return The native file descriptor or handle.
419
420 @throws std::system_error `errc::bad_file_descriptor` if the
421 file is not open. On Windows,
422 `errc::device_or_resource_busy` if an operation is still in
423 flight, or `errc::operation_not_supported` if the handle
424 cannot be detached.
425 */
426 native_handle_type release();
427
428 /** Adopt an existing native handle.
429
430 The object must be closed. To replace a held file, `close()`
431 or `release()` it first. On success the object takes
432 ownership of @p handle. Handles created elsewhere may be
433 unsuitable for asynchronous I/O. `assign()` reports most such
434 failures through the returned error code.
435
436 @param handle The native file descriptor or handle.
437
438 @return An error code describing the outcome.
439 `error::already_open` if this object is open.
440 `errc::bad_file_descriptor` if @p handle is invalid.
441 `errc::operation_not_supported` if a file object cannot
442 use it. On Windows, the rejected handles are a pipe, a
443 socket, a console, a directory, a handle opened without
444 `FILE_FLAG_OVERLAPPED`, or one
445 already in skip-completion-port-on-success mode. On
446 Windows, `errc::invalid_argument` when @p handle is
447 bound to another completion port. Any other failure is
448 the code reported by the system. Otherwise, the code is
449 empty.
450
451 @par Exception Safety
452 Throws nothing. On failure the object is unchanged and the
453 caller still owns @p handle.
454
455 @note On POSIX, the rejected descriptors are, in practice, a
456 directory, a pipe, a socket, or any other anonymous inode.
457 Adopt a pipe, a socket, or an anonymous inode into a
458 @ref posix_stream_descriptor instead. `assign()` accepts a
459 non-seekable character device such as a tty. Its first
460 read or write then fails with `ESPIPE` on the epoll,
461 kqueue, and select I/O backends.
462
463 @see release
464 */
465 [[nodiscard]] std::error_code assign(native_handle_type handle) noexcept;
466
467 protected:
468 /** Construct from a pre-built handle (for `native_random_access_file`).
469
470 @param h The pre-built handle to adopt.
471 */
472 16x explicit random_access_file(handle h) noexcept : io_object(std::move(h)) {}
473
474 private:
475 1751x inline implementation& get() const noexcept
476 {
477 1751x return *static_cast<implementation*>(h_.get());
478 }
479 };
480
481 } // namespace boost::corosio
482
483 #endif // BOOST_COROSIO_RANDOM_ACCESS_FILE_HPP
484