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/file_base.hpp>
19 : #include <boost/corosio/io/io_object.hpp>
20 : #include <boost/capy/io_result.hpp>
21 : #include <boost/capy/ex/executor_ref.hpp>
22 : #include <boost/capy/ex/execution_context.hpp>
23 : #include <boost/capy/ex/io_env.hpp>
24 : #include <boost/capy/concept/executor.hpp>
25 : #include <boost/capy/buffers.hpp>
26 :
27 : #include <concepts>
28 : #include <coroutine>
29 : #include <cstddef>
30 : #include <cstdint>
31 : #include <type_traits>
32 : #include <filesystem>
33 : #include <stop_token>
34 : #include <system_error>
35 :
36 : namespace boost::corosio {
37 :
38 : /** An asynchronous random-access file for coroutine I/O.
39 :
40 : Provides asynchronous read and write operations at explicit
41 : byte offsets, without maintaining an implicit file position.
42 :
43 : On POSIX platforms, file I/O is dispatched to a thread pool
44 : (blocking `preadv`/`pwritev`) with completion posted back to
45 : the scheduler. On Windows, true overlapped I/O is used via IOCP.
46 :
47 : @par Thread Safety
48 : Distinct objects: Safe.@n
49 : Shared objects: Unsafe. Multiple concurrent reads and writes
50 : are supported from coroutines sharing the same file object,
51 : but external synchronization is required for non-async
52 : operations (open, close, size, resize, etc.).
53 :
54 : @par Example
55 : @par !example random_access_file
56 : */
57 : class BOOST_COROSIO_DECL random_access_file : public io_object
58 : {
59 : public:
60 : /** Platform-specific random-access file implementation interface.
61 :
62 : Backends derive from this to provide offset-based file I/O.
63 : */
64 : struct implementation : io_object::implementation
65 : {
66 : /** Initiate a read at the given offset.
67 :
68 : @param offset Byte offset into the file.
69 : @param h Coroutine handle to resume on completion.
70 : @param ex Executor for dispatching the completion.
71 : @param buf The buffer to read into.
72 : @param token Stop token for cancellation.
73 : @param ec Output error code.
74 : @param bytes_out Output bytes transferred.
75 : @return Coroutine handle to resume immediately.
76 : */
77 : virtual std::coroutine_handle<> read_some_at(
78 : std::uint64_t offset,
79 : std::coroutine_handle<> h,
80 : capy::executor_ref ex,
81 : buffer_param buf,
82 : std::stop_token token,
83 : std::error_code* ec,
84 : std::size_t* bytes_out) = 0;
85 :
86 : /** Initiate a write at the given offset.
87 :
88 : @param offset Byte offset into the file.
89 : @param h Coroutine handle to resume on completion.
90 : @param ex Executor for dispatching the completion.
91 : @param buf The buffer to write from.
92 : @param token Stop token for cancellation.
93 : @param ec Output error code.
94 : @param bytes_out Output bytes transferred.
95 : @return Coroutine handle to resume immediately.
96 : */
97 : virtual std::coroutine_handle<> write_some_at(
98 : std::uint64_t offset,
99 : std::coroutine_handle<> h,
100 : capy::executor_ref ex,
101 : buffer_param buf,
102 : std::stop_token token,
103 : std::error_code* ec,
104 : std::size_t* bytes_out) = 0;
105 :
106 : /// Return the platform file descriptor or handle.
107 : virtual native_handle_type native_handle() const noexcept = 0;
108 :
109 : /// Cancel pending asynchronous operations.
110 : virtual void cancel() noexcept = 0;
111 :
112 : /// Return the file size in bytes.
113 : virtual std::uint64_t size() const = 0;
114 :
115 : /// Resize the file to @p new_size bytes.
116 : virtual std::error_code resize(std::uint64_t new_size) noexcept = 0;
117 :
118 : /// Synchronize file data to stable storage.
119 : virtual std::error_code sync_data() noexcept = 0;
120 :
121 : /// Synchronize file data and metadata to stable storage.
122 : virtual std::error_code sync_all() noexcept = 0;
123 :
124 : /// Release ownership of the native handle.
125 : virtual native_handle_type release() = 0;
126 :
127 : /// Adopt an existing native handle.
128 : virtual std::error_code assign(native_handle_type handle) noexcept = 0;
129 : };
130 :
131 : /** Awaitable for async read-at operations. */
132 : template<class MutableBufferSequence>
133 : struct read_some_at_awaitable
134 : {
135 : random_access_file& f_;
136 : std::uint64_t offset_;
137 : MutableBufferSequence buffers_;
138 : std::stop_token token_;
139 : mutable std::error_code ec_;
140 : mutable std::size_t bytes_ = 0;
141 :
142 HIT 291 : read_some_at_awaitable(
143 : random_access_file& f,
144 : std::uint64_t offset,
145 : MutableBufferSequence buffers)
146 : noexcept(std::is_nothrow_move_constructible_v<MutableBufferSequence>)
147 291 : : f_(f)
148 291 : , offset_(offset)
149 291 : , buffers_(std::move(buffers))
150 : {
151 291 : }
152 :
153 291 : bool await_ready() const noexcept
154 : {
155 : // A pre-set ec_ means the initiator failed before
156 : // dispatch (e.g. a closed object).
157 291 : return static_cast<bool>(ec_);
158 : }
159 :
160 289 : [[nodiscard]] capy::io_result<std::size_t> await_resume() const noexcept
161 : {
162 289 : return {ec_, bytes_};
163 : }
164 :
165 289 : auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
166 : -> std::coroutine_handle<>
167 : {
168 289 : token_ = env->stop_token;
169 867 : return f_.get().read_some_at(
170 867 : offset_, h, env->executor, buffers_, token_, &ec_, &bytes_);
171 : }
172 : };
173 :
174 : /** Awaitable for async write-at operations. */
175 : template<class ConstBufferSequence>
176 : struct write_some_at_awaitable
177 : {
178 : random_access_file& f_;
179 : std::uint64_t offset_;
180 : ConstBufferSequence buffers_;
181 : std::stop_token token_;
182 : mutable std::error_code ec_;
183 : mutable std::size_t bytes_ = 0;
184 :
185 41 : write_some_at_awaitable(
186 : random_access_file& f,
187 : std::uint64_t offset,
188 : ConstBufferSequence buffers)
189 : noexcept(std::is_nothrow_move_constructible_v<ConstBufferSequence>)
190 41 : : f_(f)
191 41 : , offset_(offset)
192 41 : , buffers_(std::move(buffers))
193 : {
194 41 : }
195 :
196 41 : bool await_ready() const noexcept
197 : {
198 : // A pre-set ec_ means the initiator failed before
199 : // dispatch (e.g. a closed object).
200 41 : return static_cast<bool>(ec_);
201 : }
202 :
203 41 : [[nodiscard]] capy::io_result<std::size_t> await_resume() const noexcept
204 : {
205 41 : return {ec_, bytes_};
206 : }
207 :
208 39 : auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
209 : -> std::coroutine_handle<>
210 : {
211 39 : token_ = env->stop_token;
212 117 : return f_.get().write_some_at(
213 117 : offset_, h, env->executor, buffers_, token_, &ec_, &bytes_);
214 : }
215 : };
216 :
217 : public:
218 : /** Destructor.
219 :
220 : Closes the file if open, cancelling any pending operations.
221 : */
222 : ~random_access_file() override;
223 :
224 : /** Construct from an execution context.
225 :
226 : @param ctx The execution context that will own this file.
227 : */
228 : explicit random_access_file(capy::execution_context& ctx);
229 :
230 : /** Construct from an executor.
231 :
232 : @param ex The executor whose context will own this file.
233 : */
234 : template<class Ex>
235 : requires(!std::same_as<std::remove_cvref_t<Ex>, random_access_file>) &&
236 : capy::Executor<Ex>
237 2 : explicit random_access_file(Ex const& ex) : random_access_file(ex.context())
238 : {
239 2 : }
240 :
241 : /** Move constructor. */
242 2 : random_access_file(random_access_file&& other) noexcept
243 2 : : io_object(std::move(other))
244 : {
245 2 : }
246 :
247 : /** Move assignment operator. */
248 : random_access_file& operator=(random_access_file&& other) noexcept
249 : {
250 : if (this != &other)
251 : {
252 : close();
253 : h_ = std::move(other.h_);
254 : }
255 : return *this;
256 : }
257 :
258 : random_access_file(random_access_file const&) = delete;
259 : random_access_file& operator=(random_access_file const&) = delete;
260 :
261 : /** Open a file.
262 :
263 : Failures such as a missing file or insufficient permissions
264 : are expected runtime conditions and are reported through the
265 : returned error code. If the file is already open, it is
266 : closed first.
267 :
268 : @param path The filesystem path to open.
269 : @param mode Bitmask of @ref file_base::flags specifying
270 : access mode and creation behavior.
271 :
272 : @return The error code, empty on success.
273 : */
274 : [[nodiscard]] std::error_code open(
275 : std::filesystem::path const& path,
276 : file_base::flags mode = file_base::read_only) noexcept;
277 :
278 : /** Close the file.
279 :
280 : Releases file resources. Any pending operations complete
281 : with `errc::operation_canceled`.
282 : */
283 : void close() noexcept;
284 :
285 : /** Check if the file is open. */
286 674 : bool is_open() const noexcept
287 : {
288 : #if BOOST_COROSIO_HAS_IOCP && !defined(BOOST_COROSIO_MRDOCS)
289 : return h_ && get().native_handle() != ~native_handle_type(0);
290 : #else
291 674 : return h_ && get().native_handle() >= 0;
292 : #endif
293 : }
294 :
295 : /** Read data at the given offset.
296 :
297 : @param offset Byte offset into the file.
298 : @param buffers The buffer sequence to read into.
299 :
300 : @return An awaitable yielding `(error_code, std::size_t)`.
301 :
302 : A closed file reports `errc::bad_file_descriptor`.
303 : */
304 : template<capy::MutableBufferSequence MB>
305 291 : [[nodiscard]] auto read_some_at(std::uint64_t offset, MB const& buffers)
306 : {
307 291 : read_some_at_awaitable<MB> aw(*this, offset, buffers);
308 291 : if (!is_open())
309 2 : aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
310 291 : return aw;
311 : }
312 :
313 : /** Write data at the given offset.
314 :
315 : @param offset Byte offset into the file.
316 : @param buffers The buffer sequence to write from.
317 :
318 : @return An awaitable yielding `(error_code, std::size_t)`.
319 :
320 : A closed file reports `errc::bad_file_descriptor`.
321 : */
322 : template<capy::ConstBufferSequence CB>
323 41 : [[nodiscard]] auto write_some_at(std::uint64_t offset, CB const& buffers)
324 : {
325 41 : write_some_at_awaitable<CB> aw(*this, offset, buffers);
326 41 : if (!is_open())
327 2 : aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
328 41 : return aw;
329 : }
330 :
331 : /** Cancel pending asynchronous operations. */
332 : void cancel() noexcept;
333 :
334 : /** Get the native file descriptor or handle. */
335 : native_handle_type native_handle() const noexcept;
336 :
337 : /** Return the file size in bytes.
338 :
339 : @throws std::system_error If the file is not open, or if the
340 : underlying size query fails.
341 : */
342 : std::uint64_t size() const;
343 :
344 : /** Resize the file to @p new_size bytes.
345 :
346 : Failures such as insufficient disk space are reported
347 : through the returned error code. A closed file reports
348 : `errc::bad_file_descriptor`.
349 :
350 : @param new_size The new file size.
351 :
352 : @return The error code, empty on success.
353 : */
354 : [[nodiscard]] std::error_code resize(std::uint64_t new_size) noexcept;
355 :
356 : /** Synchronize file data to stable storage.
357 :
358 : Write-back failures such as device I/O errors surface here
359 : and are reported through the returned error code. A closed
360 : file reports `errc::bad_file_descriptor`.
361 :
362 : @return The error code, empty on success.
363 : */
364 : [[nodiscard]] std::error_code sync_data() noexcept;
365 :
366 : /** Synchronize file data and metadata to stable storage.
367 :
368 : Write-back failures such as device I/O errors surface here
369 : and are reported through the returned error code. A closed
370 : file reports `errc::bad_file_descriptor`.
371 :
372 : @return The error code, empty on success.
373 : */
374 : [[nodiscard]] std::error_code sync_all() noexcept;
375 :
376 : /** Release ownership of the native handle.
377 :
378 : The file object becomes not-open. The caller is
379 : responsible for closing the returned handle.
380 :
381 : @return The native file descriptor or handle.
382 :
383 : @throws std::system_error `errc::bad_file_descriptor` if the
384 : file is not open.
385 : */
386 : native_handle_type release();
387 :
388 : /** Adopt an existing native handle.
389 :
390 : Closes any currently open file before adopting.
391 : The file object takes ownership of the handle. Handles
392 : created elsewhere may be unsuitable for asynchronous I/O;
393 : such failures are reported through the returned error code.
394 :
395 : @param handle The native file descriptor or handle.
396 :
397 : @return The error code, empty on success.
398 : */
399 : [[nodiscard]] std::error_code assign(native_handle_type handle) noexcept;
400 :
401 : protected:
402 : /// Construct from a pre-built handle (for native_random_access_file).
403 16 : explicit random_access_file(handle h) noexcept : io_object(std::move(h)) {}
404 :
405 : private:
406 1157 : inline implementation& get() const noexcept
407 : {
408 1157 : return *static_cast<implementation*>(h_.get());
409 : }
410 : };
411 :
412 : } // namespace boost::corosio
413 :
414 : #endif // BOOST_COROSIO_RANDOM_ACCESS_FILE_HPP
|