Skip to content

Buffers and journals

Overview

protobluff directly operates on the wire representation of a message, so reads and writes can be carried out without decoding and re-encoding the whole message.

In order to work with a message, the raw data must be wrapped inside a buffer for the lite runtime and inside a journal for the full runtime. Buffers are the single point of allocation inside protobluff, which means the developer is only responsible for creating and destroying the buffer (or respectively journal). This mitigates the pain of memory management, which is common in some other Protocol Buffers implementations for the C language. All other structures inside protobluff are allocated on the stack.

Journals are just buffers on steroids - they track every change made. This is crucial when working with more than one pointer (field, submessage or cursor) to a message, especially in the case of partial updates, as those pointers may get invalidated by those updates, e.g. if field offsets change.

pb_buffer_t buffer = pb_buffer_create_empty();
if (!pb_buffer_valid(&buffer)) {
  fprintf(stderr, "Error: %s", pb_error_string(pb_buffer_error(&buffer)));
} else {
  ...
  const uint8_t *data = pb_buffer_data(&buffer);
  const size_t   size = pb_buffer_size(&buffer);
  ...
}
pb_buffer_destroy(&buffer);

Buffers and journals share the same interface, as journals are just wrappers around buffers, so within the following function calls, buffer may be substituted for journal to get the equivalent function call.

Creating a buffer

An encoded Protocol Buffers message is represented as an array of bytes of a certain size:

uint8_t data[] = ...
size_t  size   = ...

In order to decode and work with this Protocol Buffers message, a protobluff buffer (or respectively journal) must be created with the encoded message's raw data and size:

pb_buffer_t buffer = pb_buffer_create(data, size);

protobluff does not take ownership of the original buffer, but copies the data to an internally allocated buffer. This is necessary, because writes to fields of the message may alter the length of the externally allocated array of bytes.

Creating a zero-copy buffer

If the original data is not expected to be altered, a zero-copy buffer can be created. protobluff will assume that the externally allocated buffer is not freed during operations:

pb_buffer_t buffer = pb_buffer_create_zero_copy(data, size);

All operations that do not impact the length of the buffer will succeed and change the original encoded Protocol Buffers message in-place, e.g. all read operations and write operations that only change the contents, but not the length of a field. Alterations that change the length of the buffer will fail and return PB_ERROR_ALLOC, as protobluff will not (and cannot) resize the externally allocated buffer.

Creating an empty buffer

If no buffer data is given, e.g. when a new Protocol Buffers message should be constructed from scratch, an empty buffer can be created:

pb_buffer_t buffer = pb_buffer_create_empty();

Freeing a buffer

When finished working with the underlying message, the buffer must be destroyed explicitly, so the internally allocated space can be freed:

pb_buffer_destroy(&buffer);

Zero-copy buffers should also always be destroyed for reasons of consistency, even if, at the time of writing, no internal allocations are taking place, as this may be subject to change in the future.

Error handling

Afar from zero-copy buffers, creation of regular buffers can theoretically fail due to an out-of-memory condition (ENOMEM). protobluff is designed to fully recover from such conditions, passing the error back to the caller after cleaning up any prior allocations. For this reason, the caller should always validate whether buffer creation was successful:

pb_buffer_t buffer = pb_buffer_create_empty();
if (!pb_buffer_valid(&buffer))
  fprintf(stderr, "Error: %s", pb_error_string(pb_buffer_error(&buffer)));

If the caller doesn't validate the buffer after creation and the buffer is not valid, all subsequent calls on messages, cursors and fields will return errors (see the section on error handling for more details).

Accessors

The buffer may not be altered directly, only by invoking the generated Protocol Buffer message accessors or the low-level interface. The raw data and size underlying the buffer can be extracted with the following methods at any time:

const uint8_t *data = pb_buffer_data(&buffer);
const size_t   size = pb_buffer_size(&buffer);

Custom allocators

protobluff is designed to allow custom allocator implementations, facilitating a more fine grained control of how memory is layed out. The default allocator is just a wrapper around the POSIX functions malloc, realloc and free. Alternative implementations may yield better performance in specific cases, like chunk allocators or fixed-size allocators. A buffer can be easily created together with a custom allocator:

pb_buffer_t buffer = pb_buffer_create_with_allocator(&allocator, data, size);

The same holds for an empty buffer:

pb_buffer_t buffer = pb_buffer_create_empty_with_allocator(&allocator);

For guidance on this topic, see the documentation on custom allocators.