BufferWriter¶
Synopsis¶
#include "swoc/BufferWriter.h"
-
class BufferWriter¶
-
class FixedBufferWriter¶
-
template<uintmax_t N>
class LocalBufferWriter¶
BufferWriter is designed for use in the common circumstance of generating formatted output strings in fixed
buffers. The goal is to replace usage that is a mixture of snprintf, strcpy, and
memcpy. BufferWriter automates buffer size checking and clipping for better reliability.
BufferWriter itself is an abstract class to describe the base interface to wrappers for various types of
output buffers. As a common example, FixedBufferWriter is a subclass that wraps a fixed
size buffer. An instance is constructed by passing it a buffer and a size, which it then tracks as
data is written. Writing past the end of the buffer is automatically clipped to prevent overruns.
BufferWriter tracks two sizes - the actual amount of space used in the buffer and the amount of data
written. The former is bounded by the buffer size to prevent overruns. The latter provides a
mechanism both to detect the clipping done to prevent overruns, and the amount of space needed to
avoid it.
Usage¶
Consider a common case of code like
char buff[1024];
char * ptr = buff;
size_t len = sizeof(buff);
//...
if (len > 0) {
auto n = std::min(len, thing1_len);
memcpy(ptr, thing1, n);
len -= n;
}
if (len > 0) {
auto n = std::min(len, thing2_len);
memcpy(ptr, thing2, n);
len -= n;
}
if (len > 0) {
auto n = std::min(len, thing3_len);
memcpy(ptr, thing3, n);
len -= n;
}
This is changed to:
char buff[1024];
swoc::FixedBufferWriter bw(buff, sizeof(buff));
//...
bw.write(thing1, thing1_len).
bw.write(thing2, thing2_len);
bw.write(thing3, thing3_len);
Or even more compactly with LocalBufferWriter
swoc::LocalBufferWriter<1024> bw;
// ...
bw.write(thing1, thing1_len).write(thing2, thing2_len).write(thing3, thing3_len);
For every call to BufferWriter::write() the remaining length is updated and checked,
discarding any overrun. This replaces the need to write the checks explictly on every memcpy.
Usually, however, BufferWriter will do the needed space checks that were not done at all previously.
A similar mechanism works for snprintf:
if (count < sizeof(buff)) {
count += snprintf(buff + count, sizeof(buff) - count, "blah blah", arg1, ...);
}
vs.
bw.commit(snprintf(bw.aux_data(), bw.remaining(), "blah blah", arg1, ...));
As before, the buffer limits are updated and checked, discarding as needed. Although
snprintf can be used in this way, BufferWriter provides its own print formatting
which is more flexible and powerful than C style printing.
BufferWriter itself is an abstract class and can’t be constructed. Use is provided through concrete
subclasses.
FixedBufferWriterThis operates on an externally defined buffer of a fixed size. The constructor requires providing the start and size of the buffer. Output is limited to that buffer.
LocalBufferWriterThis is a template class which takes a single size argument. An internal buffer of that size is made part of the instance and used as the output buffer.
FixedBufferWriter is used where the buffer is pre-existing or externally supplied. If the
buffer is only accessed by the output generation then LocalBufferWriter is more convenient,
eliminating the need to separately declare the buffer. It also makes LocalBufferWriter
usable in line, such as
std::cout << swoc::LocalBufferWriter<1024>{}.write(...).write(...).view();
which writes output to the BufferWriter instance, then gets a view of the content which is written to
std::cout.
Writing¶
The primary method for sending output to a BufferWriter is BufferWriter::write(). This is an
overloaded method with overloads for a character (char), a buffer (void *, size_t),
or a string view (std::string_view). This covers literal strings and C-style strings because
both of those implicitly convert to std::string_view. For snprintf style support,
see buffer writer formatting.
Reading¶
Data in the buffer can be extracted using BufferWriter::data(), along with
BufferWriter::size(). Together these return a pointer to the start of the buffer and the
amount of data written to the buffer. The same result can be obtained with
FixedBufferWriter::view() which returns a std::string_view which covers the output
data.
Calling BufferWriter::error() will indicate if more data than space available was written
(i.e. the buffer would have been overrun). BufferWriter::extent() returns the amount of
data written to the BufferWriter. This can be used in a two pass style with a null / size 0 buffer to
determine the buffer size required for the full output.
Advanced¶
The BufferWriter::restrict() and BufferWriter::restore() methods can be used to require space in the buffer. A common use case for this is to guarantee matching delimiters in output if buffer space is exhausted. BufferWriter::restrict() can be used to temporarily reduce the buffer capacity by an amount large enough to hold the terminal delimiter. After writing the contained output, BufferWriter::restore() can be used to restore the capacity and then output the terminal delimiter. E.g.
w.restrict(1);
w.write('[');
/// other output to w.
w.restore(1).write(']'); // always works, even if buffer was overrrun.
Warning
:libswoc::BufferWriter::restore can only restore capacity that was removed by BufferWriter::restrict(). It can not make the capacity larger than it was originally.
As something of an alternative it is easy to do “speculative” output. BufferWriter::aux_data() returns a pointer to the first byte of the buffer not yet used, and BufferWriter::remaining() returns the amount of buffer space not yet consumed. These can be easily used to create a new FixedBufferWriter on the unused space
ts::FixedBufferWriter subw(w.aux_data(), w.remaining());
Output can be written to subw. If successful BufferWriter::commit() can be used to add that output to the original buffer w
w.commit(subw.size());
If there is an error subw can be discarded and some suitable error output written to w instead. A common use case is to verify there is sufficient space in the buffer and create a “not enough space” message if not. E.g.
ts::FixedBufferWriter subw{w.aux_data(), w.remaining()};
write_some_output(subw);
if (!subw.error()) w.commit(subw.size());
else w.write("[...]");
While this can be used in a manner similar to using BufferWriter::restrict() and BufferWriter::restore() by subtracting from BufferWriter::remaining(), this can be a bit risky because the return value is unsigned and underflow would be problematic.
Examples¶
For example, error prone code that looks like
char new_via_string[1024]; // 512-bytes for hostname+via string, 512-bytes for the debug info
char * via_string = new_via_string;
char * via_limit = via_string + sizeof(new_via_string);
// ...
* via_string++ = ' ';
* via_string++ = '[';
// incoming_via can be max MAX_VIA_INDICES+1 long (i.e. around 25 or so)
if (s->txn_conf->insert_request_via_string > 2) { // Highest verbosity
via_string += nstrcpy(via_string, incoming_via);
} else {
memcpy(via_string, incoming_via + VIA_CLIENT, VIA_SERVER - VIA_CLIENT);
via_string += VIA_SERVER - VIA_CLIENT;
}
*via_string++ = ']';
becomes
ts::LocalBufferWriter<1024> w; // 1K internal buffer.
// ...
w.write(" [");
if (s->txn_conf->insert_request_via_string > 2) { // Highest verbosity
w.write(incoming_via);
} else {
w.write(std::string_view{incoming_via + VIA_CLIENT, VIA_SERVER - VIA_CLIENT});
}
w.write(']');
There will be no overrun on the memory buffer in w, in strong contrast to the original code. This can be done better, as
if (w.remaining() >= 3) {
w.restrict(1).write(" [");
if (s->txn_conf->insert_request_via_string > 2) { // Highest verbosity
w.write(incoming_via);
} else {
w.write(std::string_view{incoming_via + VIA_CLIENT, VIA_SERVER - VIA_CLIENT});
}
w.restore(1).write(']');
}
This has the result that the terminal bracket will always be present which is very much appreciated by code that parses the resulting log file.