Error handling
Overview¶
From a paranoid point of view, there's a lot that can go wrong when working with Protocol Buffers messages, e.g. allocation errors, buffer underruns or garbled data. For this reason, protobluff is designed to anticipate and catch all possible errors and to be very specific about what went wrong.
protobluff will always handle erroneous messages gracefully by design. If a buffer with an allocation error is passed to a message constructor, the message will be flagged invalid. Next, if a field to or from this invalid message is written or read, the resulting return code will indicate the same. The error will gracefully propagate through all subsequent function calls. However, to catch errors and unexpected behavior, the validity of the created structures and return values should always be checked explicitly.
Errors are represented through codes defined in the type pb_error_t
in the
public header file core/common.h
. For a detailed explanation of the specific
error codes see this section.
Errors in return values¶
All functions that return the type pb_error_t
are meant to be encapsulated
in conditional clauses. The compiler will be very whiny if this is forgotten,
thanks to the custom compiler attribute __warn_unused_result__
that is
defined for the respective functions if supported by the compiler (Clang and
GCC have had this for a long time). Though it is not recommended, this
behavior can be deactivated by compiling the linking program with the flag
-Wno-unused-result
.
If the call to a function is successful, PB_ERROR_NONE
is returned which is
set to the value 0
, POSIX-style. All other error codes have values greater
than zero. This allows easy checks for error conditions in two levels of
verbosity. If no further details regarding the error are necessary, checking
for errors is as simple as:
if (pb_validator_check(&validator, &buffer)) { /* Nope, there was an error */ }
When more information on the type of the error is needed, the following idiom should be applied:
pb_error_t error; if ((error = pb_validator_check(&validator, &buffer))) { /* Nope, there was an error */ }
The function pb_error_string
is similar to the POSIX function strerror
that returns a human-readable representation of an error code:
fprintf(stderr, "Error: %s", pb_error_string(error));
Errors upon creation¶
When creating new structures, protobluff makes heavy use of the stack where possible to minimize the need for dynamic allocations:
pb_buffer_t buffer = pb_buffer_create_empty();
Usually, libraries that use dynamic allocation and return pointers implement
error conditions by returning NULL
pointers. However, to get further details
on the type of error an extra argument to receive the type of error would have
to be passed to the function call or a global variable like errno
would have
to be used. As protobluff uses stack allocation wherever possible, a different
approach has to be taken.
For this reason, errors that happen upon creation are encoded into the resulting structure, which has two advantages opposed to passing an argument to receive the error: first, there's no need to pass an error pointer as an extra argument. Second, as stated before, the error is encoded into the structure, so subsequent calls on an erroneous structure will fail gracefully, e.g.:
/* Allocation fails, therefore buffer is invalid */ pb_buffer_t buffer = pb_buffer_create_empty(); /* Message is invalid, as it's created from an invalid buffer */ pb_message_t message = pb_message_create(&descriptor, &buffer); /* Write to field is not executed, as message is invalid */ pb_message_put(&message, 1, &value);
Nevertheless, it is highly recommended to check every function that returns a
new structure or the type pb_error_t
for errors. See the specific sections
for more details on how this should be done in the respective cases.
Error codes¶
PB_ERROR_NONE
¶
The operation was successful. This constant will always yield the value 0
.
PB_ERROR_ALLOC
¶
The required memory could not be allocated, almost only due to an out-of-memory
condition (ENOMEM
). This may happen when creating a buffer or implicitly
altering a buffer by writing to its dedicated message or fields.
PB_ERROR_INVALID
¶
The arguments passed to the function were not valid, e.g. passing an invalid buffer to a message constructor or trying to write a value to a message for a tag that is not part of the underlying message definition.
PB_ERROR_VARINT
¶
An invalid variable-sized integer was encountered, i.e. the corresponding type
is too small. If this happens, the underlying message is either garbled or the
message definitions may not be in sync. For example, a field was changed from
int32
to int64
on the encoding side, but forgotten to be updated on the
decoding side.
PB_ERROR_OFFSET
¶
This error is most likely related to garbled data, e.g. when a length prefix denoted that a message of certain length follows, but the buffer terminated unexpectedly.
PB_ERROR_ABSENT
¶
This error is returned when trying to read the value of an optional field that is not present and the field does not define a default value, or a required field is missing during message validation.
PB_ERROR_EOM
¶
Returned by pb_cursor_error
, when the cursor is past the end of the last
matching field occurrence, to indicate that the cursor in not valid anymore.