include/boost/corosio/random_access_file.hpp

100.0% Lines (40 / 40) 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) :174 522x 100.0% 100.0% boost::corosio::random_access_file::read_some_at_awaitable<boost::capy::mutable_buffer>::dispatch(std::__n4861::coroutine_handle<void>, boost::capy::executor_ref) const :188 513x 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) :215 146x 100.0% 100.0% boost::corosio::random_access_file::write_some_at_awaitable<boost::capy::const_buffer>::dispatch(std::__n4861::coroutine_handle<void>, boost::capy::executor_ref) const :229 140x 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&) :258 3x 100.0% 100.0% boost::corosio::random_access_file::random_access_file(boost::corosio::random_access_file&&) :263 3x 100.0% 100.0% boost::corosio::random_access_file::is_open() const :314 1678x 100.0% 100.0% auto boost::corosio::random_access_file::read_some_at<boost::capy::mutable_buffer>(unsigned long, boost::capy::mutable_buffer const&) :333 522x 100.0% 100.0% auto boost::corosio::random_access_file::write_some_at<boost::capy::const_buffer>(unsigned long, boost::capy::const_buffer const&) :351 146x 100.0% 100.0% boost::corosio::random_access_file::random_access_file(boost::corosio::io_object::handle) :472 24x 100.0% 100.0% boost::corosio::random_access_file::get() const :475 2737x 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/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 522x 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 522x : f_(f)
182 522x , offset_(offset)
183 522x , buffers_(std::move(buffers))
184 {
185 522x }
186
187 std::coroutine_handle<>
188 513x 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 513x cont_.h = h;
194 1026x return f_.get().read_some_at(
195 513x offset_, cont_, ex, buffers_, this->token_, &this->ec_,
196 1026x &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 146x 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 146x : f_(f)
223 146x , offset_(offset)
224 146x , buffers_(std::move(buffers))
225 {
226 146x }
227
228 std::coroutine_handle<>
229 140x dispatch(std::coroutine_handle<> h, capy::executor_ref ex) const
230 {
231 140x cont_.h = h;
232 280x return f_.get().write_some_at(
233 140x offset_, cont_, ex, buffers_, this->token_, &this->ec_,
234 280x &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 3x explicit random_access_file(Ex const& ex) : random_access_file(ex.context())
259 {
260 3x }
261
262 /** Move constructor. */
263 3x random_access_file(random_access_file&& other) noexcept
264 3x : io_object(std::move(other))
265 {
266 3x }
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 1678x 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 1678x 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 522x [[nodiscard]] auto read_some_at(std::uint64_t offset, MB const& buffers)
334 {
335 522x read_some_at_awaitable<MB> aw(*this, offset, buffers);
336 522x if (!is_open())
337 3x aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
338 522x return aw;
339 }
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 146x [[nodiscard]] auto write_some_at(std::uint64_t offset, CB const& buffers)
352 {
353 146x write_some_at_awaitable<CB> aw(*this, offset, buffers);
354 146x if (!is_open())
355 3x aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
356 146x return aw;
357 }
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 24x explicit random_access_file(handle h) noexcept : io_object(std::move(h)) {}
473
474 private:
475 2737x inline implementation& get() const noexcept
476 {
477 2737x return *static_cast<implementation*>(h_.get());
478 }
479 };
480
481 } // namespace boost::corosio
482
483 #endif // BOOST_COROSIO_RANDOM_ACCESS_FILE_HPP
484