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/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 2x explicit stream_file(Ex const& ex) : stream_file(ex.context())
150 {
151 2x }
152
153 /** Transfers ownership of the file resources.
154 */
155 2x 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 2x stream_file& operator=(stream_file&& other) noexcept
162 {
163 2x if (this != &other)
164 {
165 2x close();
166 2x h_ = std::move(other.h_);
167 }
168 2x 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 2841x 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 2841x 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 16x 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 4396x inline implementation& get() const noexcept
363 {
364 4396x return *static_cast<implementation*>(h_.get());
365 }
366 };
367
368 } // namespace boost::corosio
369
370 #endif // BOOST_COROSIO_STREAM_FILE_HPP
371