include/boost/corosio/stream_file.hpp

100.0% Lines (13 / 13) 100.0% Functions (6 / 6)
stream_file.hpp
f(x) Functions (6)
Line TLA Hits 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 2x explicit stream_file(Ex const& ex) : stream_file(ex.context())
147 {
148 2x }
149
150 /** Transfers ownership of the file resources.
151 */
152 6x 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 2x stream_file& operator=(stream_file&& other) noexcept
159 {
160 2x if (this != &other)
161 {
162 2x close();
163 2x h_ = std::move(other.h_);
164 }
165 2x 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 3058x 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 3058x 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 16x 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 4716x inline implementation& get() const noexcept
362 {
363 4716x return *static_cast<implementation*>(h_.get());
364 }
365 };
366
367 } // namespace boost::corosio
368
369 #endif // BOOST_COROSIO_STREAM_FILE_HPP
370