BufferWriter

BufferWriter is a class to write text to a buffer. The design goals are to be fast and provide buffer overrrun protection. There is a large amount of code in Traffic Server that currently uses memcpy and similar mechanisms for fast string generation. BufferWriter should be as almost as fast while simplifying code and preventing memory corruption.

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 << " [";
if (s->txn_conf->insert_request_via_string > 2) { // Highest verbosity
   w << incoming_via;
} else {
   w << ts::string_view{incoming_via + VIA_CLIENT, VIA_SERVER - VIA_CLIENT};
}
w << ']';

Note that in addition there will be no overrun on the memory buffer in w, in strong contrast to the original code.

Description

BufferWriter is an abstract super class, in the style of std::ostream. There are several subclasses for various use cases.

FixedBufferWriter writes to an external provided buffer of a fixed length. The buffer must be provided to the constructor. This will generally be used in a function where the target buffer is external to the function.

LocalBufferWriter is a templated class whose template argument is the size of an internal buffer. This is useful when the buffer is local to a function and the results will be transferred from the buffer to other storage after the output is assembled. E.g. code such as

char buff[1024];
ts::FixedBufferWriter w(buff, sizeof(buff));

is better done as

ts::LocalBufferWriter<1024> w;

Writing

The basic mechanism for writing to a BufferWriter is BufferWriter::write(). This is an overloaded method for a character (char), a buffer (void *, size_t) and a string view (ts::string_view).

On top of this mechanism are stream operators in the style of C++ stream I/O. The basic template is

template < typename T > ts::BufferWriter& operator << (ts::BufferWriter& w, T const& t);

Most basic types are overloaded and it is easy to extend. For instance, to make ts::TextView work with BufferWriter, the code would be

ts::BufferWriter & operator << (ts::BufferWriter & w, ts::TextView const & sv) {
   w.write(sv.data(), sv.size());
   return w;
}

Reading

The data in the buffer can be extracted using BufferWriter::data(). This and BufferWriter::size() return a pointer to the start of the buffer and the amount of data written to the buffer. Calling BufferWriter::error() will indicate if more data than space available was written. BufferWriter::extent() returns the amount of data written to the BufferWriter. This can be used in a two pass style with a small buffer to determine the buffer size required for the full output.

Advanced

The BufferWriter::clip() and BufferWriter::extend() methods can be used to reserve space in the buffer. A common use case for this is to guarantee matching delimiters in output if buffer space is exhausted. BufferWriter::clip() can be used to temporarily reduce the buffer size by an amount large enough to hold the terminal delimiter. After writing the contained output, BufferWriter::extend() can be used to restore the capacity and then output the terminal delimiter.

Warning

Never call BufferWriter::extend() without previoiusly calling BufferWriter::clip() and always pass the same argument value.

BufferWriter::capacity() returns the amount of buffer space not yet consumed.

BufferWriter::auxBuffer() returns a pointer to the first byte of the buffer not yet used. This is useful to do speculative output. A new BufferWriter instance can be constructed with

ts::FixedBufferWriter subw(w.auxBuffer(), w.remaining());

Output can be written to subw. If successful, then w.write(subw.size()) will add that output to the main buffer. If there is an error then subw can be ignored 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.auxBuffer(), w.remaining());
this->write_some_output(subw);
if (!subw.error()) w.write(subw.size());
else w << "Insufficient space"_sv;

Reference

class BufferWriter

BufferWriter is the abstract base class which defines the basic client interface. This is intended to be the reference type used when passing concrete instances rather than having to support the distinct types.

BufferWriter &write(void *data, size_t length)

Write to the buffer starting at data for at most length bytes. If there is not enough room to fit all the data, none is written.

BufferWriter &write(ts::string_view str)

Write the string str to the buffer. If there is not enough room to write the string no data is written.

template<size_t N>
BufferWriter &write(const char (&literal)[N])

Write literal to the output treating it as a literal string. This means the size is computed by the compiler and the null terminator is discarded. If there is not enough space in the buffer no data is written.

BufferWriter &write(char c)

Write the character c to the buffer. If there is no space in the buffer the character is not written.

char *data() const

Return a pointer to start of the buffer.

size_t size() const

Return the number of valid (written) bytes in the buffer.

size_t remaining() const

Return the number of available remaining bytes that could be written to the buffer.

size_t capacity() const

Return the number of bytes in the buffer.

char *auxBuffer() const

Return a pointer to the first byte in the buffer not yet consumed.

BufferWriter &clip(size_t n)

Reduce the available space by n bytes.

BufferWriter &extend(size_t n)

Increase the available space by n bytes. Extreme care must be used with this method as BufferWriter will trust the argument, having no way to verify it. In general this should only be used after calling BufferWriter::clip() and passing the same value. Together these allow the buffer to be temporarily reduced to reserve space for the trailing element of a required pair of output strings, e.g. making sure a closing quote can be written even if part of the string is not.

bool error() const

Return true if the buffer has overflowed from writing, false if not.

size_t extent() const

Return the total number of bytes in all attempted writes to this buffer. This value allows a successful retry in case of overflow, presuming the output data doesn’t change. This works well with the standard “try before you buy” approach of attempting to write output, counting the characters needed, then allocating a sufficiently sized buffer and actually writing.

class FixedBufferWriter : public BufferWriter

This is a class that implements BufferWriter on a fixed buffer.

FixedBufferWriter(void *buffer, size_t length)

Construct an instance that will write to buffer at most length bytes. Individual items are either written or not - if the item doesn’t fit nothing is written and the instance goes in to an error state the prevents additional output. This is so that if a large item doesn’t fit a following smaller item isn’t output out of order.

template<size_t N>
class LocalBufferWriter : public BufferWriter

This is a convenience class which is a wrapper on FixedBufferWriter which creates a buffer as a member rather than having an external buffer that is passed to the instance. The buffer is N bytes long. The point of this is to replace code like this:

char buff[1024];
FixedBufferWriter w(buff, sizeof(buff));

with:

LocalBufferWriter<1024> w;

Having the buffer size in only one place promotes more robust code.

Futures

A planned future extension is a variant of BufferWriter that operates on a MIOBuffer. This would be very useful in many places that work with MIOBuffer instances, most specifically in the body factory logic.