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/detail/op_base.hpp>
19 : #include <boost/corosio/file_base.hpp>
20 : #include <boost/corosio/io/io_object.hpp>
21 : #include <boost/capy/io_result.hpp>
22 : #include <boost/capy/ex/executor_ref.hpp>
23 : #include <boost/capy/ex/execution_context.hpp>
24 : #include <boost/capy/ex/io_env.hpp>
25 : #include <boost/capy/concept/executor.hpp>
26 : #include <boost/capy/buffers.hpp>
27 :
28 : #include <concepts>
29 : #include <coroutine>
30 : #include <cstddef>
31 : #include <cstdint>
32 : #include <type_traits>
33 : #include <filesystem>
34 : #include <stop_token>
35 : #include <system_error>
36 :
37 : namespace boost::corosio {
38 :
39 : /** Reads and writes a file at arbitrary offsets, from a coroutine.
40 :
41 : Provides asynchronous read and write operations at explicit
42 : byte offsets, without maintaining an implicit file position.
43 :
44 : On POSIX platforms, file I/O is dispatched to a thread pool
45 : (blocking `preadv`/`pwritev`) with completion posted back to
46 : the scheduler. On Windows, true overlapped I/O is used via IOCP.
47 :
48 : @par Thread Safety
49 : Distinct objects: Safe.@n
50 : Shared objects: Unsafe. Coroutines sharing the same file object may
51 : run multiple concurrent reads and writes. Non-async operations such as open, close, size, and resize require external synchronization.
52 :
53 : @par Example
54 : @par !example random_access_file
55 : */
56 : class BOOST_COROSIO_DECL random_access_file : public io_object
57 : {
58 : public:
59 : /** Declares the offset-based file operations a platform backend
60 : must implement.
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 :
117 : @param new_size The requested size in bytes.
118 :
119 : @return The error code, empty on success.
120 : */
121 : virtual std::error_code resize(std::uint64_t new_size) noexcept = 0;
122 :
123 : /** Synchronize file data to stable storage.
124 :
125 : @return The error code, empty on success.
126 : */
127 : virtual std::error_code sync_data() noexcept = 0;
128 :
129 : /** Synchronize file data and metadata to stable storage.
130 :
131 : @return The error code, empty on success.
132 : */
133 : virtual std::error_code sync_all() noexcept = 0;
134 :
135 : /// Release ownership of the native handle.
136 : virtual native_handle_type release() = 0;
137 :
138 : /** Adopt an existing native handle.
139 :
140 : @param handle The native handle to adopt. The implementation takes
141 : ownership and closes it.
142 :
143 : @return The error code, empty on success.
144 : */
145 : virtual std::error_code assign(native_handle_type handle) noexcept = 0;
146 : };
147 :
148 : /** Awaitable for async read-at operations. */
149 : template<class MutableBufferSequence>
150 : struct read_some_at_awaitable
151 : : detail::bytes_op_base<read_some_at_awaitable<MutableBufferSequence>>
152 : {
153 : private:
154 : friend random_access_file;
155 : friend detail::bytes_op_base<
156 : read_some_at_awaitable<MutableBufferSequence>>;
157 :
158 : random_access_file& f_;
159 : std::uint64_t offset_;
160 : MutableBufferSequence buffers_;
161 :
162 HIT 345 : read_some_at_awaitable(
163 : random_access_file& f,
164 : std::uint64_t offset,
165 : MutableBufferSequence
166 : buffers) noexcept(std::
167 : is_nothrow_move_constructible_v<
168 : MutableBufferSequence>)
169 345 : : f_(f)
170 345 : , offset_(offset)
171 345 : , buffers_(std::move(buffers))
172 : {
173 345 : }
174 :
175 : std::coroutine_handle<>
176 339 : dispatch(std::coroutine_handle<> h, capy::executor_ref ex) const
177 : {
178 678 : return f_.get().read_some_at(
179 339 : offset_, h, ex, buffers_, this->token_, &this->ec_,
180 678 : &this->bytes_);
181 : }
182 : };
183 :
184 : /** Awaitable for async write-at operations. */
185 : template<class ConstBufferSequence>
186 : struct write_some_at_awaitable
187 : : detail::bytes_op_base<write_some_at_awaitable<ConstBufferSequence>>
188 : {
189 : private:
190 : friend random_access_file;
191 : friend detail::bytes_op_base<
192 : write_some_at_awaitable<ConstBufferSequence>>;
193 :
194 : random_access_file& f_;
195 : std::uint64_t offset_;
196 : ConstBufferSequence buffers_;
197 :
198 93 : write_some_at_awaitable(
199 : random_access_file& f,
200 : std::uint64_t offset,
201 : ConstBufferSequence
202 : buffers) noexcept(std::
203 : is_nothrow_move_constructible_v<
204 : ConstBufferSequence>)
205 93 : : f_(f)
206 93 : , offset_(offset)
207 93 : , buffers_(std::move(buffers))
208 : {
209 93 : }
210 :
211 : std::coroutine_handle<>
212 89 : dispatch(std::coroutine_handle<> h, capy::executor_ref ex) const
213 : {
214 178 : return f_.get().write_some_at(
215 89 : offset_, h, ex, buffers_, this->token_, &this->ec_,
216 178 : &this->bytes_);
217 : }
218 : };
219 :
220 : public:
221 : /** Destructor.
222 :
223 : Closes the file if open, cancelling any pending operations.
224 : */
225 : ~random_access_file() override;
226 :
227 : /** Construct from an execution context.
228 :
229 : @param ctx The execution context that owns this file.
230 : */
231 : explicit random_access_file(capy::execution_context& ctx);
232 :
233 : /** Construct from an executor.
234 :
235 : @param ex The executor whose context owns this file.
236 : */
237 : template<class Ex>
238 : requires(!std::same_as<std::remove_cvref_t<Ex>, random_access_file>) &&
239 : capy::Executor<Ex>
240 2 : explicit random_access_file(Ex const& ex) : random_access_file(ex.context())
241 : {
242 2 : }
243 :
244 : /** Move constructor. */
245 2 : random_access_file(random_access_file&& other) noexcept
246 2 : : io_object(std::move(other))
247 : {
248 2 : }
249 :
250 : /** Move assignment operator. */
251 : random_access_file& operator=(random_access_file&& other) noexcept
252 : {
253 : if (this != &other)
254 : {
255 : close();
256 : h_ = std::move(other.h_);
257 : }
258 : return *this;
259 : }
260 :
261 : /// Copy construction is disabled; the handle is uniquely owned.
262 : random_access_file(random_access_file const&) = delete;
263 : /// Copy assignment is disabled; the handle is uniquely owned.
264 : random_access_file& operator=(random_access_file const&) = delete;
265 :
266 : /** Open a file.
267 :
268 : Failures such as a missing file or insufficient permissions
269 : are expected runtime conditions and are reported through the
270 : returned error code. If the file is already open, it is
271 : closed first.
272 :
273 : @param path The filesystem path to open.
274 : @param mode Bitmask of @ref file_base::flags specifying
275 : access mode and creation behavior.
276 :
277 : @return The error code, empty on success.
278 : */
279 : [[nodiscard]] std::error_code open(
280 : std::filesystem::path const& path,
281 : file_base::flags mode = file_base::read_only) noexcept;
282 :
283 : /** Close the file.
284 :
285 : Releases file resources. Pending operations complete through the
286 : same path as @ref cancel: one still in flight completes with
287 : `errc::operation_canceled`. An operation whose result is already
288 : decided reports that result.
289 : */
290 : void close() noexcept;
291 :
292 : /** Check if the file is open.
293 :
294 : @return `true` if the file holds an open handle.
295 : */
296 1097 : bool is_open() const noexcept
297 : {
298 : #if BOOST_COROSIO_HAS_IOCP && !defined(BOOST_COROSIO_MRDOCS)
299 : return h_ && get().native_handle() != ~native_handle_type(0);
300 : #else
301 1097 : return h_ && get().native_handle() >= 0;
302 : #endif
303 : }
304 :
305 : /** Read data at the given offset.
306 :
307 : @param offset Byte offset into the file.
308 : @param buffers The buffer sequence to read into.
309 :
310 : @return An awaitable yielding `(error_code, std::size_t)`.
311 :
312 : A closed file reports `errc::bad_file_descriptor`.
313 : */
314 : template<capy::MutableBufferSequence MB>
315 345 : [[nodiscard]] auto read_some_at(std::uint64_t offset, MB const& buffers)
316 : {
317 345 : read_some_at_awaitable<MB> aw(*this, offset, buffers);
318 345 : if (!is_open())
319 2 : aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
320 345 : return aw;
321 : }
322 :
323 : /** Write data at the given offset.
324 :
325 : @param offset Byte offset into the file.
326 : @param buffers The buffer sequence to write from.
327 :
328 : @return An awaitable yielding `(error_code, std::size_t)`.
329 :
330 : A closed file reports `errc::bad_file_descriptor`.
331 : */
332 : template<capy::ConstBufferSequence CB>
333 93 : [[nodiscard]] auto write_some_at(std::uint64_t offset, CB const& buffers)
334 : {
335 93 : write_some_at_awaitable<CB> aw(*this, offset, buffers);
336 93 : if (!is_open())
337 2 : aw.ec_ = make_error_code(std::errc::bad_file_descriptor);
338 93 : return aw;
339 : }
340 :
341 : /** Cancel pending asynchronous operations. */
342 : void cancel() noexcept;
343 :
344 : /** Get the native file descriptor or handle. */
345 : native_handle_type native_handle() const noexcept;
346 :
347 : /** Return the file size in bytes.
348 :
349 : @return The current size of the file, in bytes.
350 :
351 : @throws std::system_error If the file is not open, or if the
352 : underlying size query fails.
353 : */
354 : std::uint64_t size() const;
355 :
356 : /** Resize the file to @p new_size bytes.
357 :
358 : Failures such as insufficient disk space are reported
359 : through the returned error code. A closed file reports
360 : `errc::bad_file_descriptor`.
361 :
362 : @param new_size The new file size.
363 :
364 : @return The error code, empty on success.
365 : */
366 : [[nodiscard]] std::error_code resize(std::uint64_t new_size) noexcept;
367 :
368 : /** Synchronize file data to stable storage.
369 :
370 : Write-back failures such as device I/O errors surface here
371 : and are reported through the returned error code. A closed
372 : file reports `errc::bad_file_descriptor`.
373 :
374 : @return The error code, empty on success.
375 : */
376 : [[nodiscard]] std::error_code sync_data() noexcept;
377 :
378 : /** Synchronize file data and metadata to stable storage.
379 :
380 : Write-back failures such as device I/O errors surface here
381 : and are reported through the returned error code. A closed
382 : file reports `errc::bad_file_descriptor`.
383 :
384 : @return The error code, empty on success.
385 : */
386 : [[nodiscard]] std::error_code sync_all() noexcept;
387 :
388 : /** Release ownership of the native handle.
389 :
390 : The file object becomes not-open. The caller is
391 : responsible for closing the returned handle.
392 :
393 : @return The native file descriptor or handle.
394 :
395 : @throws std::system_error `errc::bad_file_descriptor` if the
396 : file is not open.
397 : */
398 : native_handle_type release();
399 :
400 : /** Adopt an existing native handle.
401 :
402 : Validation runs before anything is mutated or closed. On
403 : error the object still holds whatever file it held before,
404 : and the caller still owns @p handle. On success the object
405 : takes ownership of @p handle and closes any file it
406 : previously held. Handles created elsewhere may be unsuitable
407 : for asynchronous I/O; such failures are reported through the
408 : returned error code.
409 :
410 : @param handle The native file descriptor or handle.
411 :
412 : @return An error code describing the outcome. The codes
413 : that follow are those of the POSIX and io_uring
414 : backends. `errc::invalid_argument` if @p handle is the
415 : one this object already holds.
416 : `errc::bad_file_descriptor` if it is invalid.
417 : `errc::operation_not_supported` if it names something a
418 : file object cannot position. Otherwise, the `errno`
419 : reported by the kernel, or an empty code. The Windows
420 : backend validates nothing and reports the Win32 error
421 : from IOCP registration.
422 :
423 : @par Exception Safety
424 : Throws nothing. Strong guarantee.
425 :
426 : @note On POSIX, "something a file object cannot position"
427 : means, in practice, a pipe, a socket, or any other
428 : anonymous inode. Adopt those into a @ref posix_descriptor
429 : instead.
430 :
431 : @note The strong guarantee above holds on the POSIX and
432 : io_uring backends. On Windows (IOCP), a failed @p handle
433 : can still close the file this object held. That backend's
434 : file services are unified in a later stage, which is
435 : where this gap is closed.
436 :
437 : @see release
438 : */
439 : [[nodiscard]] std::error_code assign(native_handle_type handle) noexcept;
440 :
441 : protected:
442 : /// Construct from a pre-built handle (for `native_random_access_file`).
443 16 : explicit random_access_file(handle h) noexcept : io_object(std::move(h)) {}
444 :
445 : private:
446 1798 : inline implementation& get() const noexcept
447 : {
448 1798 : return *static_cast<implementation*>(h_.get());
449 : }
450 : };
451 :
452 : } // namespace boost::corosio
453 :
454 : #endif // BOOST_COROSIO_RANDOM_ACCESS_FILE_HPP
|