blob: 161dd39e92446854ddd3f71cd17bfbdd3632c2c3 [file] [log] [blame]
.. Licensed to the Apache Software Foundation (ASF) under one or more contributor license
agreements. See the NOTICE file distributed with this work for additional information regarding
copyright ownership. The ASF licenses this file to you under the Apache License, Version 2.0
(the "License"); you may not use this file except in compliance with the License. You may obtain
a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software distributed under the License
is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express
or implied. See the License for the specific language governing permissions and limitations
under the License.
.. include:: ../common-defs.rst
.. highlight:: cpp
.. default-domain:: cpp
.. |BW| replace:: :code:`BufferWriter`
.. _BufferWriter:
************
BufferWriter
************
Synopsis
********
:code:`#include "swoc/BufferWriter.h"`
.. class:: BufferWriter
:libswoc:`Reference documentation <BufferWriter>`.
.. class:: FixedBufferWriter
:libswoc:`Reference documentation <FixedBufferWriter>`.
.. class:: template < uintmax_t N > LocalBufferWriter
:libswoc:`Reference documentation <LocalBufferWriter>`.
|BW| 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 :code:`snprintf`, :code:`strcpy`, and
:code:`memcpy`. |BW| automates buffer size checking and clipping for better reliability.
|BW| itself is an abstract class to describe the base interface to wrappers for various types of
output buffers. As a common example, :libswoc:`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.
|BW| 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 :libswoc:`LocalBufferWriter` ::
swoc::LocalBufferWriter<1024> bw;
// ...
bw.write(thing1, thing1_len).write(thing2, thing2_len).write(thing3, thing3_len);
For every call to :libswoc:`BufferWriter::write` the remaining length is updated and checked,
discarding any overrun. This replaces the need to write the checks explictly on every :code:`memcpy`.
Usually, however, |BW| will do the needed space checks that were not done at all previously.
A similar mechanism works for :code:`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
:code:`snprintf` can be used in this way, |BW| provides its own `print formatting <bw-format>`_
which is more flexible and powerful than C style printing.
|BW| itself is an abstract class and can't be constructed. Use is provided through concrete
subclasses.
:class:`FixedBufferWriter`
This 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.
:class:`LocalBufferWriter`
This 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.
:class:`FixedBufferWriter` is used where the buffer is pre-existing or externally supplied. If the
buffer is only accessed by the output generation then :class:`LocalBufferWriter` is more convenient,
eliminating the need to separately declare the buffer. It also makes :class:`LocalBufferWriter`
usable in line, such as ::
std::cout << swoc::LocalBufferWriter<1024>{}.write(...).write(...).view();
which writes output to the |BW| instance, then gets a view of the content which is written to
:code:`std::cout`.
Writing
=======
The primary method for sending output to a |BW| is :libswoc:`BufferWriter::write`. This is an
overloaded method with overloads for a character (:code:`char`), a buffer (:code:`void *, size_t`),
or a string view (:code:`std::string_view`). This covers literal strings and C-style strings because
both of those implicitly convert to :code:`std::string_view`. For :code:`snprintf` style support,
see `buffer writer formatting <bw-format>`_.
Reading
=======
Data in the buffer can be extracted using :libswoc:`BufferWriter::data`, along with
:libswoc:`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
:libswoc:`FixedBufferWriter::view` which returns a :code:`std::string_view` which covers the output
data.
Calling :libswoc:`BufferWriter::error` will indicate if more data than space available was written
(i.e. the buffer would have been overrun). :libswoc:`BufferWriter::extent` returns the amount of
data written to the |BW|. 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 :libswoc:`BufferWriter::restrict` and :libswoc:`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. :libswoc:`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, :libswoc:`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
:libswoc:`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.
:libswoc:`BufferWriter::aux_data` returns a pointer to the first byte of the buffer not yet used,
and :libswoc:`BufferWriter::remaining` returns the amount of buffer space not yet consumed. These
can be easily used to create a new :libswoc:`FixedBufferWriter` on the unused space ::
ts::FixedBufferWriter subw(w.aux_data(), w.remaining());
Output can be written to :arg:`subw`. If successful :libswoc:`BufferWriter::commit` can be used to
add that output to the original buffer :arg:`w` ::
w.commit(subw.size());
If there is an error :arg:`subw` can be discarded and some suitable error output written to :arg:`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 :libswoc:`BufferWriter::restrict` and
:libswoc:`BufferWriter::restore` by subtracting from :libswoc:`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
.. code-block:: cpp
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
.. code-block:: cpp
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 :arg:`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.