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