include/boost/corosio/random_access_file.hpp

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