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