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 293 : 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 293 : : f_(f)
148 293 : , offset_(offset)
149 293 : , buffers_(std::move(buffers))
150 : {
151 293 : }
152 :
153 293 : bool await_ready() const noexcept
154 : {
155 : // A pre-set ec_ means the initiator failed before
156 : // dispatch (e.g. a closed object).
157 293 : return static_cast<bool>(ec_) || token_.stop_requested();
158 : }
159 :
160 291 : [[nodiscard]] capy::io_result<std::size_t> await_resume() const noexcept
161 : {
162 291 : if (token_.stop_requested())
163 4 : return {make_error_code(std::errc::operation_canceled), 0};
164 287 : return {ec_, bytes_};
165 : }
166 :
167 291 : auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
168 : -> std::coroutine_handle<>
169 : {
170 291 : token_ = env->stop_token;
171 873 : return f_.get().read_some_at(
172 873 : offset_, h, env->executor, buffers_, token_, &ec_, &bytes_);
173 : }
174 : };
175 :
176 : /** Awaitable for async write-at operations. */
177 : template<class ConstBufferSequence>
178 : struct write_some_at_awaitable
179 : {
180 : random_access_file& f_;
181 : std::uint64_t offset_;
182 : ConstBufferSequence buffers_;
183 : std::stop_token token_;
184 : mutable std::error_code ec_;
185 : mutable std::size_t bytes_ = 0;
186 :
187 43 : write_some_at_awaitable(
188 : random_access_file& f,
189 : std::uint64_t offset,
190 : ConstBufferSequence buffers)
191 : noexcept(std::is_nothrow_move_constructible_v<ConstBufferSequence>)
192 43 : : f_(f)
193 43 : , offset_(offset)
194 43 : , buffers_(std::move(buffers))
195 : {
196 43 : }
197 :
198 43 : bool await_ready() const noexcept
199 : {
200 : // A pre-set ec_ means the initiator failed before
201 : // dispatch (e.g. a closed object).
202 43 : return static_cast<bool>(ec_) || token_.stop_requested();
203 : }
204 :
205 43 : [[nodiscard]] capy::io_result<std::size_t> await_resume() const noexcept
206 : {
207 43 : if (token_.stop_requested())
208 2 : return {make_error_code(std::errc::operation_canceled), 0};
209 41 : return {ec_, bytes_};
210 : }
211 :
212 41 : auto await_suspend(std::coroutine_handle<> h, capy::io_env const* env)
213 : -> std::coroutine_handle<>
214 : {
215 41 : token_ = env->stop_token;
216 123 : return f_.get().write_some_at(
217 123 : offset_, h, env->executor, buffers_, token_, &ec_, &bytes_);
218 : }
219 : };
220 :
221 : public:
222 : /** Destructor.
223 :
224 : Closes the file if open, cancelling any pending operations.
225 : */
226 : ~random_access_file() override;
227 :
228 : /** Construct from an execution context.
229 :
230 : @param ctx The execution context that will own this file.
231 : */
232 : explicit random_access_file(capy::execution_context& ctx);
233 :
234 : /** Construct from an executor.
235 :
236 : @param ex The executor whose context will own this file.
237 : */
238 : template<class Ex>
239 : requires(!std::same_as<std::remove_cvref_t<Ex>, random_access_file>) &&
240 : capy::Executor<Ex>
241 2 : explicit random_access_file(Ex const& ex) : random_access_file(ex.context())
242 : {
243 2 : }
244 :
245 : /** Move constructor. */
246 2 : random_access_file(random_access_file&& other) noexcept
247 2 : : io_object(std::move(other))
248 : {
249 2 : }
250 :
251 : /** Move assignment operator. */
252 : random_access_file& operator=(random_access_file&& other) noexcept
253 : {
254 : if (this != &other)
255 : {
256 : close();
257 : h_ = std::move(other.h_);
258 : }
259 : return *this;
260 : }
261 :
262 : random_access_file(random_access_file const&) = delete;
263 : random_access_file& operator=(random_access_file const&) = delete;
264 :
265 : /** Open a file.
266 :
267 : Failures such as a missing file or insufficient permissions
268 : are expected runtime conditions and are reported through the
269 : returned error code. If the file is already open, it is
270 : closed first.
271 :
272 : @param path The filesystem path to open.
273 : @param mode Bitmask of @ref file_base::flags specifying
274 : access mode and creation behavior.
275 :
276 : @return The error code, empty on success.
277 : */
278 : [[nodiscard]] std::error_code open(
279 : std::filesystem::path const& path,
280 : file_base::flags mode = file_base::read_only) noexcept;
281 :
282 : /** Close the file.
283 :
284 : Releases file resources. Any pending operations complete
285 : with `errc::operation_canceled`.
286 : */
287 : void close() noexcept;
288 :
289 : /** Check if the file is open. */
290 682 : bool is_open() const noexcept
291 : {
292 : #if BOOST_COROSIO_HAS_IOCP && !defined(BOOST_COROSIO_MRDOCS)
293 : return h_ && get().native_handle() != ~native_handle_type(0);
294 : #else
295 682 : return h_ && get().native_handle() >= 0;
296 : #endif
297 : }
298 :
299 : /** Read data at the given offset.
300 :
301 : @param offset Byte offset into the file.
302 : @param buffers The buffer sequence to read into.
303 :
304 : @return An awaitable yielding `(error_code, std::size_t)`.
305 :
306 : A closed file reports `errc::bad_file_descriptor`.
307 : */
308 : template<capy::MutableBufferSequence MB>
309 293 : [[nodiscard]] auto read_some_at(std::uint64_t offset, MB const& buffers)
310 : {
311 293 : read_some_at_awaitable<MB> aw(*this, offset, buffers);
312 293 : if (!is_open())
313 2 : aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
314 293 : return aw;
315 : }
316 :
317 : /** Write data at the given offset.
318 :
319 : @param offset Byte offset into the file.
320 : @param buffers The buffer sequence to write from.
321 :
322 : @return An awaitable yielding `(error_code, std::size_t)`.
323 :
324 : A closed file reports `errc::bad_file_descriptor`.
325 : */
326 : template<capy::ConstBufferSequence CB>
327 43 : [[nodiscard]] auto write_some_at(std::uint64_t offset, CB const& buffers)
328 : {
329 43 : write_some_at_awaitable<CB> aw(*this, offset, buffers);
330 43 : if (!is_open())
331 2 : aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
332 43 : return aw;
333 : }
334 :
335 : /** Cancel pending asynchronous operations. */
336 : void cancel() noexcept;
337 :
338 : /** Get the native file descriptor or handle. */
339 : native_handle_type native_handle() const noexcept;
340 :
341 : /** Return the file size in bytes.
342 :
343 : @throws std::system_error If the file is not open, or if the
344 : underlying size query fails.
345 : */
346 : std::uint64_t size() const;
347 :
348 : /** Resize the file to @p new_size bytes.
349 :
350 : Failures such as insufficient disk space are reported
351 : through the returned error code. A closed file reports
352 : `errc::bad_file_descriptor`.
353 :
354 : @param new_size The new file size.
355 :
356 : @return The error code, empty on success.
357 : */
358 : [[nodiscard]] std::error_code resize(std::uint64_t new_size) noexcept;
359 :
360 : /** Synchronize file data to stable storage.
361 :
362 : Write-back failures such as device I/O errors surface here
363 : and are reported through the returned error code. A closed
364 : file reports `errc::bad_file_descriptor`.
365 :
366 : @return The error code, empty on success.
367 : */
368 : [[nodiscard]] std::error_code sync_data() noexcept;
369 :
370 : /** Synchronize file data and metadata to stable storage.
371 :
372 : Write-back failures such as device I/O errors surface here
373 : and are reported through the returned error code. A closed
374 : file reports `errc::bad_file_descriptor`.
375 :
376 : @return The error code, empty on success.
377 : */
378 : [[nodiscard]] std::error_code sync_all() noexcept;
379 :
380 : /** Release ownership of the native handle.
381 :
382 : The file object becomes not-open. The caller is
383 : responsible for closing the returned handle.
384 :
385 : @return The native file descriptor or handle.
386 :
387 : @throws std::system_error `errc::bad_file_descriptor` if the
388 : file is not open.
389 : */
390 : native_handle_type release();
391 :
392 : /** Adopt an existing native handle.
393 :
394 : Closes any currently open file before adopting.
395 : The file object takes ownership of the handle. Handles
396 : created elsewhere may be unsuitable for asynchronous I/O;
397 : such failures are reported through the returned error code.
398 :
399 : @param handle The native file descriptor or handle.
400 :
401 : @return The error code, empty on success.
402 : */
403 : [[nodiscard]] std::error_code assign(native_handle_type handle) noexcept;
404 :
405 : protected:
406 : /// Construct from a pre-built handle (for native_random_access_file).
407 16 : explicit random_access_file(handle h) noexcept : io_object(std::move(h)) {}
408 :
409 : private:
410 1171 : inline implementation& get() const noexcept
411 : {
412 1171 : return *static_cast<implementation*>(h_.get());
413 : }
414 : };
415 :
416 : } // namespace boost::corosio
417 :
418 : #endif // BOOST_COROSIO_RANDOM_ACCESS_FILE_HPP
|