TLA Line data 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 HIT 398 : 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 398 : : f_(f)
180 398 : , offset_(offset)
181 398 : , buffers_(std::move(buffers))
182 : {
183 398 : }
184 :
185 : std::coroutine_handle<>
186 392 : 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 784 : return f_.get().read_some_at(
192 392 : offset_, cont, ex, buffers_, this->token_, &this->ec_,
193 784 : &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 134 : 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 134 : : f_(f)
219 134 : , offset_(offset)
220 134 : , buffers_(std::move(buffers))
221 : {
222 134 : }
223 :
224 : std::coroutine_handle<>
225 130 : dispatch(capy::continuation& cont, capy::executor_ref ex) const
226 : {
227 260 : return f_.get().write_some_at(
228 130 : offset_, cont, ex, buffers_, this->token_, &this->ec_,
229 260 : &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 2 : explicit random_access_file(Ex const& ex) : random_access_file(ex.context())
254 : {
255 2 : }
256 :
257 : /** Move constructor. */
258 10 : random_access_file(random_access_file&& other) noexcept
259 10 : : io_object(std::move(other))
260 : {
261 10 : }
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 1302 : 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 1302 : 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 398 : [[nodiscard]] auto read_some_at(std::uint64_t offset, MB const& buffers)
329 : {
330 398 : read_some_at_awaitable<MB> aw(*this, offset, buffers);
331 398 : if (!is_open())
332 2 : aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
333 398 : 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 134 : [[nodiscard]] auto write_some_at(std::uint64_t offset, CB const& buffers)
347 : {
348 134 : write_some_at_awaitable<CB> aw(*this, offset, buffers);
349 134 : if (!is_open())
350 2 : aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
351 134 : 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 16 : explicit random_access_file(handle h) noexcept : io_object(std::move(h)) {}
470 :
471 : private:
472 2127 : inline implementation& get() const noexcept
473 : {
474 2127 : return *static_cast<implementation*>(h_.get());
475 : }
476 : };
477 :
478 : } // namespace boost::corosio
479 :
480 : #endif // BOOST_COROSIO_RANDOM_ACCESS_FILE_HPP
|