libtcspc C++ API
Streaming TCSPC and time tag data processing
Loading...
Searching...
No Matches
write_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 "errors.hpp"
12#include "introspect.hpp"
13
14#include <algorithm>
15#include <cerrno>
16#include <concepts>
17#include <cstddef>
18#include <cstdint>
19#include <cstdio>
20#include <fstream>
21#include <functional>
22#include <ios>
23#include <memory>
24#include <optional>
25#include <ostream>
26#include <span>
27#include <stdexcept>
28#include <string>
29#include <system_error>
30#include <type_traits>
31#include <utility>
32
33// When editing this file, maintain partial symmetry with
34// read_binary_stream.hpp.
35
36namespace tcspc {
37
48template <typename T>
50 std::move_constructible<T> &&
51 requires(T &s, std::span<std::byte const> buf) {
52 { s.is_error() } noexcept -> std::same_as<bool>;
53 { s.tell() } noexcept -> std::same_as<std::optional<std::uint64_t>>;
54 { s.write(buf) } noexcept -> std::same_as<void>;
55 };
56
57namespace internal {
58
60 std::uint64_t bytes_written = 0;
61
62 public:
63 static auto is_error() noexcept -> bool { return false; }
64 [[nodiscard]] auto tell() const noexcept -> std::optional<std::uint64_t> {
65 return bytes_written;
66 }
67 void write(std::span<std::byte const> buffer) noexcept {
68 bytes_written += buffer.size();
69 }
70};
71
72// We turn off ostream exceptions in the constructor.
73// NOLINTBEGIN(bugprone-exception-escape)
74template <typename OStream> class ostream_output_stream {
75 static_assert(std::is_base_of_v<std::ostream, OStream>);
76 OStream stream;
77
78 public:
79 explicit ostream_output_stream(OStream stream)
80 : stream(std::move(stream)) {
81 this->stream.exceptions(std::ios::goodbit);
82 }
83
84 auto is_error() noexcept -> bool { return stream.fail(); }
85
86 [[nodiscard]] auto tell() noexcept -> std::optional<std::uint64_t> {
87 if (stream.fail())
88 return std::nullopt; // Do not affect flags.
89 std::int64_t const pos = stream.tellp();
90 if (pos >= 0)
91 return std::uint64_t(pos);
92 stream.clear();
93 return std::nullopt;
94 }
95
96 void write(std::span<std::byte const> buffer) noexcept {
97 // NOLINTNEXTLINE(cppcoreguidelines-pro-type-reinterpret-cast)
98 stream.write(reinterpret_cast<char const *>(buffer.data()),
99 static_cast<std::streamsize>(buffer.size()));
100 }
101};
102// NOLINTEND(bugprone-exception-escape)
103
104class cfile_output_stream {
105 std::FILE *fp;
106 bool should_close;
107
108 public:
109 explicit cfile_output_stream(std::FILE *stream, bool close_on_destruction)
110 : fp(stream), should_close(close_on_destruction && fp != nullptr) {}
111
112 cfile_output_stream(cfile_output_stream const &) = delete;
113 auto operator=(cfile_output_stream const &) = delete;
114
115 cfile_output_stream(cfile_output_stream &&other) noexcept
116 : fp(std::exchange(other.fp, nullptr)),
117 should_close(std::exchange(other.should_close, false)) {}
118
119 auto operator=(cfile_output_stream &&) = delete;
120
121 ~cfile_output_stream() {
122 if (should_close)
123 (void)std::fclose(fp); // NOLINT(cppcoreguidelines-owning-memory)
124 }
125
126 auto is_error() noexcept -> bool {
127 return fp == nullptr || std::ferror(fp) != 0;
128 }
129
130 [[nodiscard]] auto tell() noexcept -> std::optional<std::uint64_t> {
131 if (fp == nullptr)
132 return std::nullopt;
133 std::int64_t pos =
134#ifdef _WIN32
135 ::_ftelli64(fp);
136#else
137 std::ftell(fp);
138#endif
139 if (pos >= 0)
140 return std::uint64_t(pos);
141 return std::nullopt;
142 }
143
144 void write(std::span<std::byte const> buffer) noexcept {
145 // Errors are checked separately by is_error(); ignore here.
146 if (fp == nullptr)
147 return;
148 (void)std::fwrite(buffer.data(), 1, buffer.size(), fp);
149 }
150};
151
152// For benchmarking only
153inline auto
154unbuffered_binary_ofstream_output_stream(std::string const &filename,
155 arg::truncate<bool> truncate,
156 arg::append<bool> append) {
157 std::ofstream stream;
158
159 // Set to unbuffered.
160 stream.rdbuf()->pubsetbuf(nullptr, 0);
161
162 stream.open(filename,
163 std::ios::binary |
164 (truncate.value ? std::ios::trunc : std::ios::openmode{}) |
165 (append.value ? std::ios::ate : std::ios::openmode{}));
166 if (stream.fail())
167 throw input_output_error("failed to open output file: " + filename);
168 return internal::ostream_output_stream(std::move(stream));
169}
170
171// For benchmarking only
172inline auto binary_ofstream_output_stream(std::string const &filename,
173 arg::truncate<bool> truncate,
174 arg::append<bool> append) {
175 std::ofstream stream;
176 stream.open(filename,
177 std::ios::binary |
178 (truncate.value ? std::ios::trunc : std::ios::openmode{}) |
179 (append.value ? std::ios::ate : std::ios::openmode{}));
180 if (stream.fail())
181 throw input_output_error("failed to open output file: " + filename);
182 return internal::ostream_output_stream(std::move(stream));
183}
184
185inline auto unbuffered_binary_cfile_output_stream(std::string const &filename,
186 arg::truncate<bool> truncate,
187 arg::append<bool> append) {
188 char const *mode = std::invoke([&] {
189 if (truncate.value)
190 return "wb";
191 if (append.value)
192 return "ab";
193 return "wbx";
194 });
195#ifdef _WIN32 // Avoid requiring _CRT_SECURE_NO_WARNINGS.
196 std::FILE *fp{};
197 (void)fopen_s(&fp, filename.c_str(), mode);
198#else
199 errno = 0; // ISO C does not require fopen to set errno on error.
200 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
201 std::FILE *fp = std::fopen(filename.c_str(), mode);
202#endif
203 if (fp == nullptr) {
204 if (errno != 0)
205 throw std::system_error(errno, std::generic_category());
206 throw input_output_error("failed to open output file: " + filename);
207 }
208 if (std::setvbuf(fp, nullptr, _IONBF, 0) != 0)
209 throw input_output_error(
210 "failed to disable buffering for output file: " + filename);
211 return internal::cfile_output_stream(fp, true);
212}
213
214// For benchmarking only
215inline auto binary_cfile_output_stream(std::string const &filename,
216 arg::truncate<bool> truncate,
217 arg::append<bool> append) {
218 char const *mode = std::invoke([&] {
219 if (truncate.value)
220 return "wb";
221 if (append.value)
222 return "ab";
223 return "wbx";
224 });
225#ifdef _WIN32 // Avoid requiring _CRT_SECURE_NO_WARNINGS.
226 std::FILE *fp{};
227 (void)fopen_s(&fp, filename.c_str(), mode);
228#else
229 errno = 0; // ISO C does not require fopen to set errno on error.
230 // NOLINTNEXTLINE(cppcoreguidelines-owning-memory)
231 std::FILE *fp = std::fopen(filename.c_str(), mode);
232#endif
233 if (fp == nullptr) {
234 if (errno != 0)
235 throw std::system_error(errno, std::generic_category());
236 throw input_output_error("failed to open output file: " + filename);
237 }
238 return internal::cfile_output_stream(fp, true);
239}
240
241} // namespace internal
242
250inline auto null_output_stream() { return internal::null_output_stream(); }
251
270inline auto
271binary_file_output_stream(std::string const &filename,
272 arg::truncate<bool> truncate = arg::truncate{false},
273 arg::append<bool> append = arg::append{false}) {
274 return internal::unbuffered_binary_cfile_output_stream(filename, truncate,
275 append);
276}
277
292template <typename OStream> inline auto ostream_output_stream(OStream stream) {
293 static_assert(std::is_base_of_v<std::ostream, OStream>);
294 return internal::ostream_output_stream(std::move(stream));
295}
296
315inline auto owning_cfile_output_stream(std::FILE *fp) {
316 return internal::cfile_output_stream(fp, true);
317}
318
341inline auto borrowed_cfile_output_stream(std::FILE *fp) {
342 return internal::cfile_output_stream(fp, false);
343}
344
345namespace internal {
346
347template <typename OutputStream>
348 requires output_stream<OutputStream>
350 OutputStream strm;
351 std::shared_ptr<bucket_source<std::byte>> bsource;
352 std::size_t write_granularity;
353
354 std::uint64_t total_bytes_written = 0;
355
356 // If not empty, buffer to use next, containing a partial event:
357 bucket<std::byte> buffer;
358 std::size_t bytes_buffered = 0;
359
360 void handle_span(std::span<std::byte const> event_span) {
361 auto first_block_size = write_granularity;
362 if (total_bytes_written == 0) {
363 // Align second and subsequent writes to write_granularity if
364 // current offset is available. This may or may not improve
365 // write performance (when the write_granularity is a multiple
366 // of the page size or block size), but shouldn't hurt.
367 std::optional<std::uint64_t> pos = strm.tell();
368 if (pos.has_value()) {
369 first_block_size =
370 write_granularity - *pos % write_granularity;
371 }
372 }
373
374 if (bytes_buffered > 0 || first_block_size < write_granularity) {
375 auto const bytes_available =
376 std::min(bytes_buffered + event_span.size(), first_block_size);
377 if (buffer.empty())
378 buffer = bsource->bucket_of_size(write_granularity);
379 auto const dest_span =
380 buffer.first(bytes_available).subspan(bytes_buffered);
381 auto const src_span = event_span.first(
382 std::min(event_span.size(), dest_span.size()));
383 std::copy(src_span.begin(), src_span.end(), dest_span.begin());
384 if (bytes_available == first_block_size) {
385 strm.write(buffer.first(bytes_available));
386 buffer = {};
387 bytes_buffered = 0;
388 if (strm.is_error())
389 throw input_output_error("failed to write output");
390 total_bytes_written += bytes_available;
391 } else {
392 bytes_buffered = bytes_available;
393 }
394 event_span = event_span.subspan(src_span.size());
395 }
396
397 auto const direct_write_size =
398 event_span.size() / write_granularity * write_granularity;
399 if (direct_write_size > 0) {
400 strm.write(event_span.first(direct_write_size));
401 if (strm.is_error())
402 throw input_output_error("failed to write output");
403 total_bytes_written += direct_write_size;
404 event_span = event_span.subspan(direct_write_size);
405 }
406
407 if (not event_span.empty()) {
408 buffer = bsource->bucket_of_size(write_granularity);
409 bytes_buffered = event_span.size();
410 std::copy(event_span.begin(), event_span.end(), buffer.begin());
411 }
412 }
413
414 public:
415 explicit write_binary_stream(
416 OutputStream stream,
417 std::shared_ptr<bucket_source<std::byte>> buffer_provider,
418 arg::granularity<std::size_t> granularity)
419 : strm(std::move(stream)), bsource(std::move(buffer_provider)),
420 write_granularity(granularity.value) {
421 if (not bsource)
422 throw std::invalid_argument(
423 "write_binary_stream buffer_provider must not be null");
424 if (write_granularity <= 0)
425 throw std::invalid_argument(
426 "write_binary_stream granularity must be positive");
427 }
428
429 [[nodiscard]] auto introspect_node() const -> processor_info {
430 return processor_info(this, "write_binary_stream");
431 }
432
433 [[nodiscard]] auto introspect_graph() const -> processor_graph {
434 return processor_graph().push_entry_point(this);
435 }
436
437 template <typename Span,
438 typename = std::void_t<
439 decltype(std::span<std::byte const>(std::declval<Span>()))>>
440 void handle(Span const &event) {
441 handle_span(std::span<std::byte const>(event));
442 }
443
444 void flush() {
445 if (bytes_buffered > 0) {
446 strm.write(std::span(buffer).first(bytes_buffered));
447 buffer = {};
448 if (strm.is_error())
449 throw input_output_error("failed to write output");
450 }
451 }
452};
453
454} // namespace internal
455
508template <typename OutputStream>
510 OutputStream stream,
511 std::shared_ptr<bucket_source<std::byte>> buffer_provider,
512 arg::granularity<std::size_t> granularity) {
513 // Support direct passing of C++ iostreams stream.
514 if constexpr (std::is_base_of_v<std::ostream, OutputStream>) {
515 auto wrapped = ostream_output_stream(std::move(stream));
516 return internal::write_binary_stream<decltype(wrapped)>(
517 std::move(wrapped), std::move(buffer_provider), granularity);
518 } else {
519 return internal::write_binary_stream<OutputStream>(
520 std::move(stream), std::move(buffer_provider), granularity);
521 }
522}
523
524} // namespace tcspc
Concept that is satisfied when T conforms to the libtcspc output stream interface.
Definition write_binary_stream.hpp:49
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 append(Event event, Downstream downstream)
Create a processor that inserts an event at the end of the stream.
Definition prepend_append.hpp:154
auto write_binary_stream(OutputStream stream, std::shared_ptr< bucket_source< std::byte > > buffer_provider, arg::granularity< std::size_t > granularity)
Create a sink that writes bytes to a binary stream, such as a file.
Definition write_binary_stream.hpp:509
auto binary_file_output_stream(std::string const &filename, arg::truncate< bool > truncate=arg::truncate{false}, arg::append< bool > append=arg::append{false})
Create a binary output stream for the given file.
Definition write_binary_stream.hpp:271
auto borrowed_cfile_output_stream(std::FILE *fp)
Create an output stream from a non-owned C file pointer.
Definition write_binary_stream.hpp:341
auto owning_cfile_output_stream(std::FILE *fp)
Create an output stream from a C file pointer, taking ownership.
Definition write_binary_stream.hpp:315
auto null_output_stream()
Create an output stream that discards all written bytes.
Definition write_binary_stream.hpp:250
auto ostream_output_stream(OStream stream)
Create an output stream from an std::ostream instance.
Definition write_binary_stream.hpp:292
libtcspc namespace.
Definition acquire.hpp:30
Function argument wrapper for granularity parameter.
Definition arg_wrappers.hpp:147
Function argument wrapper for truncate parameter.
Definition arg_wrappers.hpp:427
Abstract base class for polymorphic bucket sources.
Definition bucket.hpp:504