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