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) :173 391x 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 :187 385x 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) :212 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 :226 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&) :254 2x 100.0% 100.0% boost::corosio::random_access_file::random_access_file(boost::corosio::random_access_file&&) :259 10x 100.0% 100.0% boost::corosio::random_access_file::is_open() const :310 1250x 100.0% 100.0% auto boost::corosio::random_access_file::read_some_at<boost::capy::mutable_buffer>(unsigned long, boost::capy::mutable_buffer const&) :329 391x 100.0% 100.0% auto boost::corosio::random_access_file::write_some_at<boost::capy::const_buffer>(unsigned long, boost::capy::const_buffer const&) :347 134x 100.0% 100.0% boost::corosio::random_access_file::random_access_file(boost::corosio::io_object::handle) :470 16x 100.0% 100.0% boost::corosio::random_access_file::get() const :473 2041x 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/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/results.hpp>
24 #include <boost/capy/ex/executor_ref.hpp>
25 #include <boost/capy/ex/execution_context.hpp>
26 #include <boost/capy/ex/await_context.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
173 391x read_some_at_awaitable(
174 random_access_file& f,
175 std::uint64_t offset,
176 MutableBufferSequence
177 buffers) noexcept(std::
178 is_nothrow_move_constructible_v<
179 MutableBufferSequence>)
180 391x : f_(f)
181 391x , offset_(offset)
182 391x , buffers_(std::move(buffers))
183 {
184 391x }
185
186 std::coroutine_handle<>
187 385x dispatch(capy::continuation& cont, capy::executor_ref ex) const
188 {
189 // The continuation lives in the awaiting frame, which stays
190 // put until resumption -- unlike the per-call op, which is
191 // freed before the coroutine runs.
192 770x return f_.get().read_some_at(
193 385x offset_, cont, ex, buffers_, this->token_, &this->ec_,
194 770x &this->bytes_);
195 }
196 };
197
198 /** Awaitable for async write-at operations. */
199 template<class ConstBufferSequence>
200 struct write_some_at_awaitable
201 : detail::bytes_op_base<write_some_at_awaitable<ConstBufferSequence>>
202 {
203 private:
204 friend random_access_file;
205 friend detail::bytes_op_base<
206 write_some_at_awaitable<ConstBufferSequence>>;
207
208 random_access_file& f_;
209 std::uint64_t offset_;
210 ConstBufferSequence buffers_;
211
212 134x write_some_at_awaitable(
213 random_access_file& f,
214 std::uint64_t offset,
215 ConstBufferSequence
216 buffers) noexcept(std::
217 is_nothrow_move_constructible_v<
218 ConstBufferSequence>)
219 134x : f_(f)
220 134x , offset_(offset)
221 134x , buffers_(std::move(buffers))
222 {
223 134x }
224
225 std::coroutine_handle<>
226 130x dispatch(capy::continuation& cont, capy::executor_ref ex) const
227 {
228 260x return f_.get().write_some_at(
229 130x offset_, cont, ex, buffers_, this->token_, &this->ec_,
230 260x &this->bytes_);
231 }
232 };
233
234 public:
235 /** Destructor.
236
237 Closes the file if open, cancelling any pending operations.
238 */
239 ~random_access_file() override;
240
241 /** Construct from an execution context.
242
243 @param ctx The execution context that owns this file.
244 */
245 explicit random_access_file(capy::execution_context& ctx);
246
247 /** Construct from an executor.
248
249 @param ex The executor whose context owns this file.
250 */
251 template<class Ex>
252 requires(!std::same_as<std::remove_cvref_t<Ex>, random_access_file>) &&
253 capy::Executor<Ex>
254 2x explicit random_access_file(Ex const& ex) : random_access_file(ex.context())
255 {
256 2x }
257
258 /** Move constructor. */
259 10x random_access_file(random_access_file&& other) noexcept
260 10x : io_object(std::move(other))
261 {
262 10x }
263
264 /** Move assignment operator. */
265 random_access_file& operator=(random_access_file&& other) noexcept
266 {
267 if (this != &other)
268 {
269 close();
270 h_ = std::move(other.h_);
271 }
272 return *this;
273 }
274
275 /// Copy construction is disabled; the handle is uniquely owned.
276 random_access_file(random_access_file const&) = delete;
277 /// Copy assignment is disabled; the handle is uniquely owned.
278 random_access_file& operator=(random_access_file const&) = delete;
279
280 /** Open a file.
281
282 Failures such as a missing file or insufficient permissions
283 are expected runtime conditions and are reported through the
284 returned error code. If the file is already open, it is
285 closed first.
286
287 @param path The filesystem path to open.
288 @param mode Bitmask of @ref file_base::flags specifying
289 access mode and creation behavior.
290
291 @return The error code, empty on success.
292 */
293 [[nodiscard]] std::error_code open(
294 std::filesystem::path const& path,
295 file_base::flags mode = file_base::read_only) noexcept;
296
297 /** Close the file.
298
299 Releases file resources. Pending operations complete through the
300 same path as @ref cancel: one still in flight completes with
301 `errc::operation_canceled`. An operation whose result is already
302 decided reports that result.
303 */
304 void close() noexcept;
305
306 /** Check if the file is open.
307
308 @return `true` if the file holds an open handle.
309 */
310 1250x bool is_open() const noexcept
311 {
312 #if BOOST_COROSIO_HAS_IOCP && !defined(BOOST_COROSIO_MRDOCS)
313 return h_ && get().native_handle() != ~native_handle_type(0);
314 #else
315 1250x return h_ && get().native_handle() >= 0;
316 #endif
317 }
318
319 /** Read data at the given offset.
320
321 @param offset Byte offset into the file.
322 @param buffers The buffer sequence to read into.
323
324 @return An awaitable yielding `(error_code, std::size_t)`.
325
326 A closed file reports `errc::bad_file_descriptor`.
327 */
328 template<capy::MutableBufferSequence MB>
329 391x [[nodiscard]] auto read_some_at(std::uint64_t offset, MB const& buffers)
330 {
331 391x read_some_at_awaitable<MB> aw(*this, offset, buffers);
332 391x if (!is_open())
333 2x aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
334 391x return aw;
335 }
336
337 /** Write data at the given offset.
338
339 @param offset Byte offset into the file.
340 @param buffers The buffer sequence to write from.
341
342 @return An awaitable yielding `(error_code, std::size_t)`.
343
344 A closed file reports `errc::bad_file_descriptor`.
345 */
346 template<capy::ConstBufferSequence CB>
347 134x [[nodiscard]] auto write_some_at(std::uint64_t offset, CB const& buffers)
348 {
349 134x write_some_at_awaitable<CB> aw(*this, offset, buffers);
350 134x if (!is_open())
351 2x aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
352 134x return aw;
353 }
354
355 /** Cancel pending asynchronous operations. */
356 void cancel() noexcept;
357
358 /** Get the native file descriptor or handle. */
359 native_handle_type native_handle() const noexcept;
360
361 /** Return the file size in bytes.
362
363 @return The current size of the file, in bytes.
364
365 @throws std::system_error If the file is not open, or if the
366 underlying size query fails.
367 */
368 std::uint64_t size() const;
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 The file object becomes not-open. The caller is
405 responsible for closing the returned handle.
406
407 `release()` cancels pending operations first. On Windows, the
408 object keeps the handle and this throws if one is still in
409 flight. It does the same if Windows refuses to detach the
410 handle from the execution context's completion port. Where
411 file I/O runs on a thread pool, it throws likewise while a
412 worker thread is already performing an operation. Call
413 `release()` again once the cancelled operations have
414 completed. Detaching requires Windows 8.1 or later.
415
416 @return The native file descriptor or handle.
417
418 @throws std::system_error `errc::bad_file_descriptor` if the
419 file is not open, or `errc::device_or_resource_busy` if an
420 operation is still using the handle, as described above. On
421 Windows, it also throws `errc::operation_not_supported` if
422 the handle cannot be detached.
423 */
424 native_handle_type release();
425
426 /** Adopt an existing native handle.
427
428 The object must be closed. To replace a held file, `close()`
429 or `release()` it first. On success the object takes
430 ownership of @p handle. Handles created elsewhere may be
431 unsuitable for asynchronous I/O. `assign()` reports most such
432 failures through the returned error code.
433
434 @param handle The native file descriptor or handle.
435
436 @return An error code describing the outcome.
437 `error::already_open` if this object is open.
438 `errc::bad_file_descriptor` if @p handle is invalid.
439 `errc::operation_not_supported` if a file object cannot
440 use it. On Windows, the rejected handles are a pipe, a
441 socket, a console, a directory, a handle opened without
442 `FILE_FLAG_OVERLAPPED`, or one
443 already in skip-completion-port-on-success mode. On
444 Windows, `errc::invalid_argument` when @p handle is
445 bound to another completion port. Any other failure is
446 the code reported by the system. Otherwise, the code is
447 empty.
448
449 @par Exception Safety
450 Throws nothing. On failure the object is unchanged and the
451 caller still owns @p handle.
452
453 @note On POSIX, the rejected descriptors are, in practice, a
454 directory, a pipe, a socket, or any other anonymous inode.
455 Adopt a pipe, a socket, or an anonymous inode into a
456 @ref posix_stream_descriptor instead. `assign()` accepts a
457 non-seekable character device such as a tty. Its first
458 read or write then fails with `ESPIPE` on the epoll,
459 kqueue, and select I/O backends.
460
461 @see release
462 */
463 [[nodiscard]] std::error_code assign(native_handle_type handle) noexcept;
464
465 protected:
466 /** Construct from a pre-built handle (for `native_random_access_file`).
467
468 @param h The pre-built handle to adopt.
469 */
470 16x explicit random_access_file(handle h) noexcept : io_object(std::move(h)) {}
471
472 private:
473 2041x inline implementation& get() const noexcept
474 {
475 2041x return *static_cast<implementation*>(h_.get());
476 }
477 };
478
479 } // namespace boost::corosio
480
481 #endif // BOOST_COROSIO_RANDOM_ACCESS_FILE_HPP
482