guanaqo 1.0.0-alpha.27
Utilities for scientific software
Loading...
Searching...
No Matches
callback-streambuf.hpp
Go to the documentation of this file.
1#pragma once
2
3/// @file
4/// @ingroup io
5/// Stream buffer and redirector for capturing ostream output.
6
7#include <cstring>
8#include <functional>
9#include <ostream>
10#include <span>
11#include <streambuf>
12#include <vector>
13
14namespace guanaqo {
15
16/// An implementation of a `std::streambuf` that calls the given callback
17/// function with the characters that are written.
18/// @note Not thread-safe.
19///
20/// Inspired by https://github.com/pybind/pybind11/blob/master/include/pybind11/iostream.h
21/// @ingroup io
22class callback_streambuf : public std::streambuf {
23 public:
24 using write_func_t = std::function<void(std::span<const char>)>;
25
26 private:
27 /// Computes how many bytes at the end of the buffer are part of an
28 /// incomplete sequence of UTF-8 bytes.
29 /// @pre `pbase() < pptr()`
30 [[nodiscard]] size_t utf8_remainder() const {
31 const auto rbase = std::reverse_iterator<char *>(pbase());
32 const auto rpptr = std::reverse_iterator<char *>(pptr());
33 static auto uch = [](char c) { return static_cast<unsigned char>(c); };
34 auto is_ascii = [](char c) { return (uch(c) & 0x80) == 0x00; };
35 auto is_leading = [](char c) { return (uch(c) & 0xC0) == 0xC0; };
36 auto is_leading_2b = [](char c) { return uch(c) <= 0xDF; };
37 auto is_leading_3b = [](char c) { return uch(c) <= 0xEF; };
38 // If the last character is ASCII, there are no incomplete code points
39 if (is_ascii(*rpptr))
40 return 0;
41 // Otherwise, work back from the end of the buffer and find the first
42 // UTF-8 leading byte
43 const auto rpend = rbase - rpptr >= 3 ? rpptr + 3 : rbase;
44 const auto leading = std::find_if(rpptr, rpend, is_leading);
45 if (leading == rbase)
46 return 0;
47 const auto dist = static_cast<size_t>(leading - rpptr);
48 size_t remainder = 0;
49
50 if (dist == 0)
51 remainder = 1; // 1-byte code point is impossible
52 else if (dist == 1)
53 remainder = is_leading_2b(*leading) ? 0 : dist + 1;
54 else if (dist == 2)
55 remainder = is_leading_3b(*leading) ? 0 : dist + 1;
56 // else if (dist >= 3), at least 4 bytes before encountering an UTF-8
57 // leading byte, either no remainder or invalid UTF-8.
58 // We do not intend to handle invalid UTF-8 here.
59 return remainder;
60 }
61
62 /// Calls @ref write_func if the buffer is not empty.
63 int _sync() {
64 if (pbase() != pptr()) { // If buffer is not empty
65 // This subtraction cannot be negative, so dropping the sign.
66 auto size = static_cast<size_t>(pptr() - pbase());
67 size_t remainder = utf8_remainder();
68
69 if (size > remainder)
70 write_func(std::span{pbase(), size - remainder});
71
72 // Copy the remainder at the end of the buffer to the beginning:
73 if (remainder > 0)
74 std::memmove(pbase(), pptr() - remainder, remainder);
75 setp(pbase(), epptr());
76 pbump(static_cast<int>(remainder));
77 }
78 return 0;
79 }
80
81 int sync() override { return _sync(); }
82
83 int overflow(int c) override {
84 using traits_type = std::streambuf::traits_type;
85 if (!traits_type::eq_int_type(c, traits_type::eof())) {
86 *pptr() = traits_type::to_char_type(c);
87 pbump(1);
88 }
89 return _sync() == 0 ? traits_type::not_eof(c) : traits_type::eof();
90 }
91
92 public:
93 callback_streambuf(write_func_t write_func, size_t buffer_size = 1024)
94 : write_func{std::move(write_func)} {
95 buffer.resize(buffer_size);
96 setp(buffer.data(), buffer.data() + buffer.size());
97 }
98
99 /// Syncs before destroy
100 ~callback_streambuf() override { _sync(); }
101
102 private:
104 std::vector<char> buffer;
105};
106
107/// Temporarily replaces the rdbuf of the given ostream. Flushes and restores
108/// the old rdbuf upon destruction.
109/// @ingroup io
111 private:
112 std::ostream &os;
113 std::streambuf *old_buf;
114
115 public:
116 explicit scoped_ostream_redirect(std::ostream &os, std::streambuf *rdbuf)
117 : os{os}, old_buf{os.rdbuf(rdbuf)} {}
118
120 os.flush();
121 os.rdbuf(old_buf);
122 }
123
129};
130
131} // namespace guanaqo
callback_streambuf(write_func_t write_func, size_t buffer_size=1024)
~callback_streambuf() override
Syncs before destroy.
std::function< void(std::span< const char >)> write_func_t
size_t utf8_remainder() const
Computes how many bytes at the end of the buffer are part of an incomplete sequence of UTF-8 bytes.
scoped_ostream_redirect(std::ostream &os, std::streambuf *rdbuf)
scoped_ostream_redirect & operator=(scoped_ostream_redirect &&)=delete
scoped_ostream_redirect(const scoped_ostream_redirect &)=delete
scoped_ostream_redirect(scoped_ostream_redirect &&other)=default
scoped_ostream_redirect & operator=(const scoped_ostream_redirect &)=delete