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_STREAM_FILE_HPP
11 : #define BOOST_COROSIO_STREAM_FILE_HPP
12 :
13 : #include <boost/corosio/detail/config.hpp>
14 : #include <boost/corosio/detail/platform.hpp>
15 : #include <boost/corosio/detail/native_handle.hpp>
16 : #include <boost/corosio/error.hpp>
17 : #include <boost/corosio/file_base.hpp>
18 : #include <boost/corosio/io/io_stream.hpp>
19 : #include <boost/capy/ex/execution_context.hpp>
20 : #include <boost/capy/concept/executor.hpp>
21 : #include <boost/capy/results.hpp>
22 :
23 : #include <concepts>
24 : #include <cstdint>
25 : #include <filesystem>
26 : #include <system_error>
27 :
28 : namespace boost::corosio {
29 :
30 : /** Reads and writes a file sequentially, from a coroutine.
31 :
32 : Provides asynchronous read and write operations on a regular
33 : file with an implicit position that advances after each
34 : operation.
35 :
36 : Inherits from @ref io_stream, so `read_some` and `write_some`
37 : are available and work with any algorithm that accepts an
38 : `io_stream&`.
39 :
40 : On POSIX platforms, file I/O is dispatched to a thread pool
41 : (blocking `preadv`/`pwritev`) with completion posted back to
42 : the scheduler. On Windows, true overlapped I/O is used via IOCP.
43 :
44 : On Windows, while the file is open, its handle is bound to the
45 : execution context's completion port. Every overlapped call on the
46 : handle queues a packet to that port. Do not issue your own
47 : overlapped I/O on `native_handle()` (`DeviceIoControl`,
48 : `ReadFile`) unless the `OVERLAPPED`'s `hEvent` has its low-order
49 : bit set, which suppresses the packet.
50 :
51 : @par Thread Safety
52 : Distinct objects: Safe.@n
53 : Shared objects: Unsafe. Only one asynchronous operation
54 : may be in flight at a time.
55 :
56 : @par Example
57 : @par !example stream_file
58 : */
59 : class BOOST_COROSIO_DECL stream_file : public io_stream
60 : {
61 : public:
62 : /** Defines the file operations a platform backend implements.
63 :
64 : Backends derive from this to provide file I/O.
65 : `read_some` and `write_some` are inherited from
66 : @ref io_stream::implementation.
67 : */
68 : struct implementation : io_stream::implementation
69 : {
70 : /// Return the platform file descriptor or handle.
71 : virtual native_handle_type native_handle() const noexcept = 0;
72 :
73 : /// Cancel pending asynchronous operations.
74 : virtual void cancel() noexcept = 0;
75 :
76 : /** Return the file size in bytes.
77 :
78 : @return The error code and the current size, in bytes.
79 : */
80 : virtual capy::results<std::uint64_t> size() const noexcept = 0;
81 :
82 : /** Resize the file to @p new_size bytes.
83 :
84 : @param new_size The requested size in bytes.
85 :
86 : @return The error code, empty on success.
87 : */
88 : virtual std::error_code resize(std::uint64_t new_size) noexcept = 0;
89 :
90 : /** Synchronize file data to stable storage.
91 :
92 : @return The error code, empty on success.
93 : */
94 : virtual std::error_code sync_data() noexcept = 0;
95 :
96 : /** Synchronize file data and metadata to stable storage.
97 :
98 : @return The error code, empty on success.
99 : */
100 : virtual std::error_code sync_all() noexcept = 0;
101 :
102 : /** Release ownership of the native handle.
103 :
104 : On success the caller owns the returned handle.
105 :
106 : @return The error code and the native handle.
107 : */
108 : virtual capy::results<native_handle_type> release() noexcept = 0;
109 :
110 : /** Adopt an existing native handle.
111 :
112 : @param handle The native handle to adopt. The implementation takes
113 : ownership and closes it.
114 :
115 : @return The error code, empty on success.
116 : */
117 : virtual std::error_code assign(native_handle_type handle) noexcept = 0;
118 :
119 : /** Move the file position.
120 :
121 : @param offset Signed offset from @p origin.
122 : @param origin The reference point for the seek.
123 : @return The error code and new absolute position.
124 : */
125 : virtual capy::results<std::uint64_t>
126 : seek(std::int64_t offset, file_base::seek_basis origin) noexcept = 0;
127 : };
128 :
129 : /** Closes the file if open, cancelling any pending operations.
130 : */
131 : ~stream_file() override;
132 :
133 : /** Construct from an execution context.
134 :
135 : @param ctx The execution context that owns this file.
136 : */
137 : explicit stream_file(capy::execution_context& ctx);
138 :
139 : /** Construct from an executor.
140 :
141 : @param ex The executor whose context owns this file.
142 : */
143 : template<class Ex>
144 : requires(!std::same_as<std::remove_cvref_t<Ex>, stream_file>) &&
145 : capy::Executor<Ex>
146 HIT 2 : explicit stream_file(Ex const& ex) : stream_file(ex.context())
147 : {
148 2 : }
149 :
150 : /** Transfers ownership of the file resources.
151 : */
152 6 : stream_file(stream_file&& other) noexcept : io_object(std::move(other)) {}
153 :
154 : /** Closes any existing file and transfers ownership.
155 :
156 : @return Reference to this object.
157 : */
158 2 : stream_file& operator=(stream_file&& other) noexcept
159 : {
160 2 : if (this != &other)
161 : {
162 2 : close();
163 2 : h_ = std::move(other.h_);
164 : }
165 2 : return *this;
166 : }
167 :
168 : /// Copy construction is disabled; the handle is uniquely owned.
169 : stream_file(stream_file const&) = delete;
170 : /// Copy assignment is disabled; the handle is uniquely owned.
171 : stream_file& operator=(stream_file const&) = delete;
172 :
173 : // read_some() inherited from io_read_stream
174 : // write_some() inherited from io_write_stream
175 :
176 : /** Open a file.
177 :
178 : Failures such as a missing file or insufficient permissions
179 : are expected runtime conditions and are reported through the
180 : returned error code. If the file is already open, it is
181 : closed first.
182 :
183 : @param path The filesystem path to open.
184 : @param mode Bitmask of @ref file_base::flags specifying
185 : access mode and creation behavior.
186 :
187 : @return The error code, empty on success.
188 : */
189 : [[nodiscard]] std::error_code open(
190 : std::filesystem::path const& path,
191 : file_base::flags mode = file_base::read_only) noexcept;
192 :
193 : /** Close the file.
194 :
195 : Releases file resources. Pending operations complete through the
196 : same path as @ref cancel: one still in flight completes with
197 : `errc::operation_canceled`. An operation whose result is already
198 : decided reports that result.
199 : */
200 : void close() noexcept;
201 :
202 : /** Check if the file is open.
203 :
204 : @return `true` if the file is open and ready for I/O.
205 : */
206 3058 : bool is_open() const noexcept
207 : {
208 : #if BOOST_COROSIO_HAS_IOCP && !defined(BOOST_COROSIO_MRDOCS)
209 : return h_ && get().native_handle() != ~native_handle_type(0);
210 : #else
211 3058 : return h_ && get().native_handle() >= 0;
212 : #endif
213 : }
214 :
215 : /** Cancel pending asynchronous operations.
216 :
217 : Operations still in flight complete with
218 : `errc::operation_canceled`; an operation whose result is
219 : already decided reports that result.
220 : */
221 : void cancel() noexcept;
222 :
223 : /** Get the native file descriptor or handle.
224 :
225 : @return The native handle, or -1/INVALID_HANDLE_VALUE
226 : if not open.
227 : */
228 : native_handle_type native_handle() const noexcept;
229 :
230 : /** Return the file size in bytes.
231 :
232 : A closed file reports `errc::bad_file_descriptor`. Other
233 : failures carry the error code from the operating system. On
234 : failure the size is zero.
235 :
236 : @return The error code and the file size in bytes.
237 : */
238 : [[nodiscard]] capy::results<std::uint64_t> size() const noexcept;
239 :
240 : /** Resize the file to @p new_size bytes.
241 :
242 : Failures such as insufficient disk space are reported
243 : through the returned error code. A closed file reports
244 : `errc::bad_file_descriptor`.
245 :
246 : @param new_size The new file size.
247 :
248 : @return The error code, empty on success.
249 : */
250 : [[nodiscard]] std::error_code resize(std::uint64_t new_size) noexcept;
251 :
252 : /** Synchronize file data to stable storage.
253 :
254 : Write-back failures such as device I/O errors surface here
255 : and are reported through the returned error code. A closed
256 : file reports `errc::bad_file_descriptor`.
257 :
258 : @return The error code, empty on success.
259 : */
260 : [[nodiscard]] std::error_code sync_data() noexcept;
261 :
262 : /** Synchronize file data and metadata to stable storage.
263 :
264 : Write-back failures such as device I/O errors surface here
265 : and are reported through the returned error code. A closed
266 : file reports `errc::bad_file_descriptor`.
267 :
268 : @return The error code, empty on success.
269 : */
270 : [[nodiscard]] std::error_code sync_all() noexcept;
271 :
272 : /** Release ownership of the native handle.
273 :
274 : On success the file object becomes not-open, and the caller
275 : is responsible for closing the returned handle.
276 :
277 : `release()` cancels pending operations first. It reports
278 : `errc::device_or_resource_busy` if one is still in flight on
279 : Windows. Where file I/O runs on a thread pool, it reports the
280 : same while a worker thread is already performing one. Call
281 : `release()` again once the cancelled operations have completed. On
282 : Windows it reports `errc::operation_not_supported` if the
283 : handle cannot be detached from the execution context's
284 : completion port. Detaching requires Windows 8.1 or later.
285 :
286 : A closed file reports `errc::bad_file_descriptor`. On
287 : failure the object keeps the handle and the returned handle
288 : is the invalid sentinel ( -1 or `INVALID_HANDLE_VALUE` ), never
289 : zero.
290 :
291 : @return The error code and the native file descriptor or handle.
292 : */
293 : [[nodiscard]] capy::results<native_handle_type> release() noexcept;
294 :
295 : /** Adopt an existing native handle.
296 :
297 : The object must be closed. To replace a held file, `close()`
298 : or `release()` it first. On success the object takes
299 : ownership of @p handle. Handles created elsewhere may be
300 : unsuitable for asynchronous I/O. `assign()` reports most such
301 : failures through the returned error code.
302 :
303 : @param handle The native file descriptor or handle.
304 :
305 : @return An error code describing the outcome.
306 : `error::already_open` if this object is open.
307 : `errc::bad_file_descriptor` if @p handle is invalid.
308 : `errc::operation_not_supported` if a file object cannot
309 : use it. On Windows, the rejected handles are a pipe, a
310 : socket, a console, a directory, a handle opened without
311 : `FILE_FLAG_OVERLAPPED`, or one
312 : already in skip-completion-port-on-success mode. On
313 : Windows, `errc::invalid_argument` when @p handle is
314 : bound to another completion port. Any other failure is
315 : the code reported by the system. Otherwise, the code is
316 : empty.
317 :
318 : @par Exception Safety
319 : Throws nothing. On failure the object is unchanged and the
320 : caller still owns @p handle.
321 :
322 : @note On POSIX, the rejected descriptors are, in practice, a
323 : directory, a pipe, a socket, or any other anonymous inode.
324 : Adopt a pipe, a socket, or an anonymous inode into a
325 : @ref posix_stream_descriptor instead. `assign()` accepts a
326 : non-seekable character device such as a tty. Its first
327 : read or write then fails with `ESPIPE` on the epoll,
328 : kqueue, and select I/O backends.
329 :
330 : @see @ref release
331 : */
332 : [[nodiscard]] std::error_code assign(native_handle_type handle) noexcept;
333 :
334 : /** Move the file position.
335 :
336 : Positions beyond the end of the file are allowed. A
337 : resulting negative position is reported through the error
338 : code, as offsets often originate from file contents. A
339 : closed file reports `errc::bad_file_descriptor`.
340 :
341 : @param offset Signed offset from @p origin.
342 : @param origin The reference point for the seek.
343 :
344 : @return The error code and new absolute position.
345 : */
346 : [[nodiscard]] capy::results<std::uint64_t> seek(
347 : std::int64_t offset,
348 : file_base::seek_basis origin = file_base::seek_set) noexcept;
349 :
350 : protected:
351 : /// Default-construct (for derived types that initialize `io_object` directly).
352 16 : stream_file() noexcept = default;
353 :
354 : /** Construct from a pre-built handle (for `native_stream_file`).
355 :
356 : @param h The pre-built handle to adopt.
357 : */
358 : explicit stream_file(handle h) noexcept : io_object(std::move(h)) {}
359 :
360 : private:
361 4716 : inline implementation& get() const noexcept
362 : {
363 4716 : return *static_cast<implementation*>(h_.get());
364 : }
365 : };
366 :
367 : } // namespace boost::corosio
368 :
369 : #endif // BOOST_COROSIO_STREAM_FILE_HPP
|