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