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/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 HIT 349 : 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 349 : : f_(f)
182 349 : , offset_(offset)
183 349 : , buffers_(std::move(buffers))
184 : {
185 349 : }
186 :
187 : std::coroutine_handle<>
188 343 : 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 343 : cont_.h = h;
194 686 : return f_.get().read_some_at(
195 343 : offset_, cont_, ex, buffers_, this->token_, &this->ec_,
196 686 : &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 97 : 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 97 : : f_(f)
223 97 : , offset_(offset)
224 97 : , buffers_(std::move(buffers))
225 : {
226 97 : }
227 :
228 : std::coroutine_handle<>
229 93 : dispatch(std::coroutine_handle<> h, capy::executor_ref ex) const
230 : {
231 93 : cont_.h = h;
232 186 : return f_.get().write_some_at(
233 93 : offset_, cont_, ex, buffers_, this->token_, &this->ec_,
234 186 : &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 2 : explicit random_access_file(Ex const& ex) : random_access_file(ex.context())
259 : {
260 2 : }
261 :
262 : /** Move constructor. */
263 2 : random_access_file(random_access_file&& other) noexcept
264 2 : : io_object(std::move(other))
265 : {
266 2 : }
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 1116 : 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 1116 : 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 349 : [[nodiscard]] auto read_some_at(std::uint64_t offset, MB const& buffers)
334 : {
335 349 : read_some_at_awaitable<MB> aw(*this, offset, buffers);
336 349 : if (!is_open())
337 2 : aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
338 349 : 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 97 : [[nodiscard]] auto write_some_at(std::uint64_t offset, CB const& buffers)
352 : {
353 97 : write_some_at_awaitable<CB> aw(*this, offset, buffers);
354 97 : if (!is_open())
355 2 : aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
356 97 : 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 16 : explicit random_access_file(handle h) noexcept : io_object(std::move(h)) {}
473 :
474 : private:
475 1817 : inline implementation& get() const noexcept
476 : {
477 1817 : return *static_cast<implementation*>(h_.get());
478 : }
479 : };
480 :
481 : } // namespace boost::corosio
482 :
483 : #endif // BOOST_COROSIO_RANDOM_ACCESS_FILE_HPP
|