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