libtcspc C++ API
Streaming TCSPC and time tag data processing
Loading...
Searching...
No Matches
read_binary_stream.hpp
1/*
2 * This file is part of libtcspc
3 * Copyright 2019-2026 Board of Regents of the University of Wisconsin System
4 * SPDX-License-Identifier: MIT
5 */
6
7#pragma once
8
9#include "arg_wrappers.hpp"
10#include "bucket.hpp"
11#include "common.hpp"
12#include "core.hpp"
13#include "errors.hpp"
14#include "int_types.hpp"
15#include "introspect.hpp"
16#include "processor.hpp"
17
18#include <algorithm>
19#include <cerrno>
20#include <concepts>
21#include <cstddef>
22#include <cstdint>
23#include <cstdio>
24#include <fstream>
25#include <ios>
26#include <istream>
27#include <limits>
28#include <memory>
29#include <optional>
30#include <span>
31#include <stdexcept>
32#include <string>
33#include <system_error>
34#include <type_traits>
35#include <utility>
36#include <vector>
37
38// When editing this file, maintain partial symmetry with
39// write_binary_stream.hpp.
40
41namespace tcspc {
42
54template <typename T>
55concept input_stream =
56 std::move_constructible<T> &&
57 requires(T &s, std::uint64_t bytes, std::span<std::byte> buf) {
58 { s.is_error() } noexcept -> std::same_as<bool>;
59 { s.is_eof() } noexcept -> std::same_as<bool>;
60 { s.is_good() } noexcept -> std::same_as<bool>;
61 { s.tell() } noexcept -> std::same_as<std::optional<std::uint64_t>>;
62 { s.skip(bytes) } noexcept -> std::same_as<bool>;
63 { s.read(buf) } noexcept -> std::same_as<std::uint64_t>;
64 };
65
66namespace internal {
67
68struct null_input_stream {
69 static auto is_error() noexcept -> bool { return false; }
70 static auto is_eof() noexcept -> bool { return true; }
71 static auto is_good() noexcept -> bool { return false; }
72 static auto tell() noexcept -> std::optional<std::uint64_t> { return 0; }
73 static auto skip(std::uint64_t bytes) noexcept -> bool {
74 return bytes == 0;
75 }
76 static auto read(std::span<std::byte> /* buffer */) noexcept
77 -> std::uint64_t {
78 return 0;
79 }
80};
81
82// We turn off istream exceptions in the constructor.
83// NOLINTBEGIN(bugprone-exception-escape)
84template <typename IStream> class istream_input_stream {
85 static_assert(std::is_base_of_v<std::istream, IStream>);
86 IStream stream;
87
88 public:
89 explicit istream_input_stream(IStream stream) : stream(std::move(stream)) {
90 this->stream.exceptions(std::ios::goodbit);
91 }
92
93 auto is_error() noexcept -> bool {
94 auto const flags = stream.rdstate();
95 return ((flags & std::ios::failbit) || (flags & std::ios::badbit)) &&
96 not(flags & std::ios::eofbit);
97 }
98
99 auto is_eof() noexcept -> bool { return stream.eof(); }
100
101 auto is_good() noexcept -> bool { return stream.good(); }
102
103 auto tell() noexcept -> std::optional<std::uint64_t> {
104 if (stream.fail())
105 return std::nullopt; // Do not affect flags.
106 std::int64_t const pos = stream.tellg();
107 if (pos >= 0)
108 return std::uint64_t(pos);
109 stream.clear();
110 return std::nullopt;
111 }
112
113 auto skip(std::uint64_t bytes) noexcept -> bool {
114 if (stream.fail() ||
115 bytes > std::uint64_t(std::numeric_limits<std::streamoff>::max()))
116 return false;
117 stream.seekg(std::streamoff(bytes), std::ios::cur);
118 auto const ret = stream.good();
119 stream.clear();
120 return ret;
121 }
122
123 auto read(std::span<std::byte> buffer) noexcept -> std::uint64_t {
124 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
125 stream.read(reinterpret_cast<char *>(buffer.data()),
126 static_cast<std::streamsize>(buffer.size()));
127 return static_cast<std::uint64_t>(stream.gcount());
128 }
129};
130// NOLINTEND(bugprone-exception-escape)
131
132class cfile_input_stream {
133 std::FILE *fp;
134 bool should_close;
135
136 public:
137 explicit cfile_input_stream(std::FILE *stream, bool close_on_destruction)
138 : fp(stream), should_close(close_on_destruction && fp != nullptr) {}
139
140 cfile_input_stream(cfile_input_stream const &) = delete;
141 auto operator=(cfile_input_stream const &) = delete;
142
143 cfile_input_stream(cfile_input_stream &&other) noexcept
144 : fp(std::exchange(other.fp, nullptr)),
145 should_close(std::exchange(other.should_close, false)) {}
146
147 auto operator=(cfile_input_stream &&) = delete;
148
149 ~cfile_input_stream() {
150 if (should_close)
151 (void)std::fclose(fp); // NOLINT(cppcoreguidelines-owning-memory)
152 }
153
154 auto is_error() noexcept -> bool {
155 return fp == nullptr || std::ferror(fp) != 0;
156 }
157
158 auto is_eof() noexcept -> bool {
159 return fp != nullptr && std::feof(fp) != 0;
160 }
161
162 auto is_good() noexcept -> bool {
163 return fp != nullptr && std::ferror(fp) == 0 && std::feof(fp) == 0;
164 }
165
166 auto tell() noexcept -> std::optional<std::uint64_t> {
167 if (fp == nullptr)
168 return std::nullopt;
169 std::int64_t pos =
170#ifdef _WIN32
171 ::_ftelli64(fp);
172#else
173 std::ftell(fp);
174#endif
175 if (pos >= 0)
176 return std::uint64_t(pos);
177 return std::nullopt;
178 }
179
180 auto skip(std::uint64_t bytes) noexcept -> bool {
181 if (fp == nullptr)
182 return false;
183#ifdef _WIN32
184 if (bytes <= std::uint64_t(std::numeric_limits<__int64>::max()))
185 return ::_fseeki64(fp, __int64(bytes), SEEK_CUR) == 0;
186#else
187 if (bytes <= std::numeric_limits<long>::max())
188 return std::fseek(fp, long(bytes), SEEK_CUR) == 0;
189#endif
190 return false;
191 }
192
193 auto read(std::span<std::byte> buffer) noexcept -> std::uint64_t {
194 if (fp == nullptr)
195 return 0;
196 return std::fread(buffer.data(), 1, buffer.size(), fp);
197 }
198};
199
200template <typename InputStream>
201 requires input_stream<InputStream>
202inline void skip_stream_bytes(InputStream &stream, std::uint64_t bytes) {
203 if (not stream.skip(bytes)) {
204 // Try instead reading and discarding up to 'start', to support
205 // non-seekable streams (e.g., pipes).
206 std::uint64_t bytes_discarded = 0;
207 // For now, use the read size that was found fastest when reading
208 // /dev/zero on an Apple M1 Pro laptop. Could be tuned.
209 static constexpr std::streamsize bufsize = 32768;
210 std::vector<std::byte> buf(bufsize);
211 std::span<std::byte> const bufspan(buf);
212 while (bytes_discarded < bytes) {
213 auto read_size =
214 std::min<std::uint64_t>(bufsize, bytes - bytes_discarded);
215 bytes_discarded += stream.read(bufspan.first(read_size));
216 if (not stream.is_good())
217 break;
218 }
219 }
220}
221
222// For benchmarking only
223inline auto
224unbuffered_binary_ifstream_input_stream(std::string const &filename,
225 arg::start_offset<u64> start_offset) {
226 std::ifstream stream;
227
228 // The standard says that the following makes the stream "unbuffered", but
229 // its definition of unbuffered specifies nothing about input streams. At
230 // least with libc++, this is a huge pessimization:
231 stream.rdbuf()->pubsetbuf(nullptr, 0);
232
233 stream.open(filename, std::ios::binary);
234 if (stream.fail())
235 throw input_output_error("failed to open input file: " + filename);
236 auto ret = internal::istream_input_stream(std::move(stream));
237 skip_stream_bytes(ret, start_offset.value);
238 return ret;
239}
240
241// For benchmarking only
242inline auto binary_ifstream_input_stream(std::string const &filename,
243 arg::start_offset<u64> start_offset) {
244 std::ifstream stream;
245 stream.open(filename, std::ios::binary);
246 if (stream.fail())
247 throw input_output_error("failed to open input file: " + filename);
248 auto ret = internal::istream_input_stream(std::move(stream));
249 skip_stream_bytes(ret, start_offset.value);
250 return ret;
251}
252
253inline auto unbuffered_binary_cfile_input_stream(
254 std::string const &filename,
255 arg::start_offset<std::uint64_t> start_offset) {
256#ifdef _WIN32 // Avoid requiring _CRT_SECURE_NO_WARNINGS.
257 std::FILE *fp{};
258 (void)fopen_s(&fp, filename.c_str(), "rb");
259#else
260 errno = 0; // ISO C does not require fopen to set errno on error.
261 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
262 std::FILE *fp = std::fopen(filename.c_str(), "rb");
263#endif
264 if (fp == nullptr) {
265 if (errno != 0)
266 throw std::system_error(errno, std::generic_category());
267 throw input_output_error("failed to open input file: " + filename);
268 }
269 if (std::setvbuf(fp, nullptr, _IONBF, 0) != 0)
270 throw input_output_error(
271 "failed to disable buffering for input file: " + filename);
272 auto ret = internal::cfile_input_stream(fp, true);
273 skip_stream_bytes(ret, start_offset.value);
274 return ret;
275}
276
277// For benchmarking only
278inline auto binary_cfile_input_stream(std::string const &filename,
279 arg::start_offset<u64> start_offset) {
280#ifdef _WIN32 // Avoid requiring _CRT_SECURE_NO_WARNINGS.
281 std::FILE *fp{};
282 (void)fopen_s(&fp, filename.c_str(), "rb");
283#else
284 errno = 0; // ISO C does not require fopen to set errno on error.
285 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
286 std::FILE *fp = std::fopen(filename.c_str(), "rb");
287#endif
288 if (fp == nullptr) {
289 if (errno != 0)
290 throw std::system_error(errno, std::generic_category());
291 throw input_output_error("failed to open input file: " + filename);
292 }
293 auto ret = internal::cfile_input_stream(fp, true);
294 skip_stream_bytes(ret, start_offset.value);
295 return ret;
296}
297
298} // namespace internal
299
307inline auto null_input_stream() { return internal::null_input_stream(); }
308
324 std::string const &filename,
325 arg::start_offset<u64> start_offset = arg::start_offset{u64(0)}) {
326 // Prefer cfile over ifstream for performance; for cfile, unbuffered
327 // performs better (given our own buffering). See benchmark.
328 return internal::unbuffered_binary_cfile_input_stream(filename,
329 start_offset);
330}
331
349template <typename IStream> inline auto istream_input_stream(IStream stream) {
350 static_assert(std::is_base_of_v<std::istream, IStream>);
351 return internal::istream_input_stream(std::move(stream));
352}
353
372inline auto owning_cfile_input_stream(std::FILE *fp) {
373 return internal::cfile_input_stream(fp, true);
374}
375
398inline auto borrowed_cfile_input_stream(std::FILE *fp) {
399 return internal::cfile_input_stream(fp, false);
400}
401
402namespace internal {
403
404template <typename InputStream, typename Event, typename Downstream>
405 requires input_stream<InputStream> &&
406 processor<Downstream, bucket<Event>, warning_event>
407class read_binary_stream {
408 static_assert(
409 std::is_trivial_v<Event>,
410 "Event type must be trivial to work with read_binary_stream");
411
412 InputStream stream;
413 std::uint64_t length;
414
415 std::size_t read_granularity;
416 std::shared_ptr<bucket_source<Event>> bsource;
417
418 Downstream downstream;
419
420 LIBTCSPC_NOINLINE auto first_read_size() -> std::uint64_t {
421 auto ret = read_granularity;
422 if (stream.is_good()) {
423 // Align second and subsequent reads to read_granularity if current
424 // offset is available. This may or may not improve read
425 // performance (when the read_granularity is a multiple of the page
426 // size or block size), but shouldn't hurt.
427 std::optional<std::uint64_t> pos = stream.tell();
428 if (pos.has_value())
429 ret -= *pos % read_granularity;
430 }
431 return ret;
432 }
433
434 // Read some multiple (max: max_units) of the read granularity that fits in
435 // dest, subject to first-read size and max length.
436 auto read_units(std::span<std::byte> dest, std::size_t max_units,
437 std::uint64_t &total_bytes_read) -> std::uint64_t {
438 auto bytes_to_read =
439 std::min<std::uint64_t>(dest.size(), length - total_bytes_read);
440 if (total_bytes_read == 0) {
441 bytes_to_read =
442 std::min<std::uint64_t>(bytes_to_read, first_read_size());
443 }
444 if (bytes_to_read > read_granularity) {
445 bytes_to_read = read_granularity *
446 std::min<std::uint64_t>(
447 max_units, bytes_to_read / read_granularity);
448 }
449 auto const bytes_read = stream.read(dest.first(bytes_to_read));
450 total_bytes_read += bytes_read;
451 return bytes_read;
452 }
453
454 public:
455 explicit read_binary_stream(
456 InputStream stream, arg::max_length<std::uint64_t> max_length,
457 std::shared_ptr<bucket_source<Event>> buffer_provider,
458 arg::granularity<std::size_t> granularity, Downstream downstream)
459 : stream(std::move(stream)), length(max_length.value),
460 read_granularity(granularity.value),
461 bsource(std::move(buffer_provider)),
462 downstream(std::move(downstream)) {
463 if (not bsource)
464 throw std::invalid_argument(
465 "read_binary_stream buffer_provider must not be null");
466 if (read_granularity <= 0)
467 throw std::invalid_argument(
468 "read_binary_stream granularity must be positive");
469 }
470
471 [[nodiscard]] auto introspect_node() const -> processor_info {
472 return processor_info(this, "read_binary_stream");
473 }
474
475 [[nodiscard]] auto introspect_graph() const -> processor_graph {
476 return downstream.introspect_graph().push_entry_point(this);
477 }
478
479 void flush() {
480 auto const bucket_size =
481 sizeof(Event) >= read_granularity
482 ? 1
483 : (read_granularity - 1) / sizeof(Event) + 1;
484 auto const bucket_size_bytes = bucket_size * sizeof(Event);
485
486 std::uint64_t total_bytes_read = 0;
487 bucket<Event> bkt;
488 std::size_t remainder_nbytes = 0; // Always < sizeof(Event)
489
490 while (total_bytes_read < length && stream.is_good()) {
491 auto const bytes_left_in_bucket =
492 bucket_size_bytes - remainder_nbytes;
493 if (bytes_left_in_bucket >= read_granularity) {
494 if (bkt.empty())
495 bkt = bsource->bucket_of_size(bucket_size);
496 auto const bytes_read = read_units(
497 std::as_writable_bytes(std::span(bkt))
498 .subspan(remainder_nbytes),
499 std::numeric_limits<std::size_t>::max(), total_bytes_read);
500 auto const available_nbytes = remainder_nbytes + bytes_read;
501 auto const this_batch_size = available_nbytes / sizeof(Event);
502 remainder_nbytes = available_nbytes % sizeof(Event);
503 if (this_batch_size == 0)
504 continue; // Leave incomplete event in current bucket.
505 bucket<Event> bkt2;
506 if (remainder_nbytes > 0) {
507 bkt2 = bsource->bucket_of_size(bucket_size);
508 auto const remainder_span =
509 std::as_bytes(std::span(bkt))
510 .subspan(available_nbytes - remainder_nbytes);
511 std::copy(remainder_span.begin(), remainder_span.end(),
512 std::as_writable_bytes(std::span(bkt2)).begin());
513 }
514 bkt.shrink(0, this_batch_size);
515 downstream.handle(std::move(bkt));
516 bkt = std::move(bkt2);
517 } else { // Top off single event.
518 bucket<Event> bkt2;
519 bkt2 = bsource->bucket_of_size(bucket_size);
520 auto const bytes_read =
521 read_units(std::as_writable_bytes(std::span(bkt2)), 1,
522 total_bytes_read);
523 if (bytes_read < bytes_left_in_bucket)
524 break;
525 auto const top_off_span =
526 std::as_bytes(std::span(bkt2)).first(bytes_left_in_bucket);
527 auto const remainder_span = std::as_bytes(std::span(bkt2))
528 .first(bytes_read)
529 .subspan(bytes_left_in_bucket);
530 std::copy(top_off_span.begin(), top_off_span.end(),
531 std::as_writable_bytes(std::span(bkt))
532 .last(bytes_left_in_bucket)
533 .begin());
534 std::copy(remainder_span.begin(), remainder_span.end(),
535 std::as_writable_bytes(std::span(bkt2)).begin());
536 downstream.handle(std::move(bkt));
537 bkt = std::move(bkt2);
538 remainder_nbytes = remainder_span.size();
539 }
540 }
541
542 if (stream.is_error())
543 throw input_output_error("failed to read input");
544 if (remainder_nbytes > 0) {
545 downstream.handle(warning_event{
546 "bytes fewer than record size remain at end of input"});
547 }
548 downstream.flush();
549 }
550};
551
552} // namespace internal
553
613template <typename Event, typename InputStream, typename Downstream>
614auto read_binary_stream(InputStream stream,
616 std::shared_ptr<bucket_source<Event>> buffer_provider,
618 Downstream downstream) {
619 // Support direct passing of C++ iostreams stream.
620 if constexpr (std::is_base_of_v<std::istream, InputStream>) {
621 auto wrapped = istream_input_stream(std::move(stream));
622 return internal::read_binary_stream<decltype(wrapped), Event,
623 Downstream>(
624 std::move(wrapped), max_length, std::move(buffer_provider),
625 granularity, std::move(downstream));
626 } else {
627 return internal::read_binary_stream<InputStream, Event, Downstream>(
628 std::move(stream), max_length, std::move(buffer_provider),
629 granularity, std::move(downstream));
630 }
631}
632
633} // namespace tcspc
Concept that is satisfied when T conforms to the libtcspc input stream interface.
Definition read_binary_stream.hpp:55
auto buffer(arg::threshold< std::size_t > threshold, access_tracker< buffer_accessor > &&tracker, Downstream downstream)
Create a processor that buffers events and emits them on a different thread.
Definition buffer.hpp:366
auto read_binary_stream(InputStream stream, arg::max_length< std::uint64_t > max_length, std::shared_ptr< bucket_source< Event > > buffer_provider, arg::granularity< std::size_t > granularity, Downstream downstream)
Create a source that reads batches of events from a binary stream, such as a file.
Definition read_binary_stream.hpp:614
auto null_input_stream()
Create an input stream that contains no bytes.
Definition read_binary_stream.hpp:307
auto istream_input_stream(IStream stream)
Create an input stream from an std::istream instance.
Definition read_binary_stream.hpp:349
auto owning_cfile_input_stream(std::FILE *fp)
Create an input stream from a C file pointer, taking ownership.
Definition read_binary_stream.hpp:372
auto binary_file_input_stream(std::string const &filename, arg::start_offset< u64 > start_offset=arg::start_offset{u64(0)})
Create a binary input stream for the given file.
Definition read_binary_stream.hpp:323
auto borrowed_cfile_input_stream(std::FILE *fp)
Create an input stream from a non-owned C file pointer.
Definition read_binary_stream.hpp:398
std::uint64_t u64
Short name for uint64_t.
Definition int_types.hpp:33
libtcspc namespace.
Definition acquire.hpp:30
Function argument wrapper for granularity parameter.
Definition arg_wrappers.hpp:147
Function argument wrapper for maximum length parameter.
Definition arg_wrappers.hpp:267
Function argument wrapper for start offset parameter.
Definition arg_wrappers.hpp:377
Abstract base class for polymorphic bucket sources.
Definition bucket.hpp:504