|
Lite³
A JSON-Compatible Zero-Copy Serialization Format
|
The previous guide was about strings. This guide will explain nested structures (object hierarchies).
Nesting allows applications to represent complex relationships that would be difficult to represent with only flat datastructures.
This guide is based on an example inside the Lite³ repository found in examples/context_api/04-nesting.c:
Output:
We will walk through the example code step-by-step, explaining the use of Lite³ library functions.
Lite³ messages are just bytes, stored contiguously inside a buffer. If you want to allocate these messages inside your own custom allocators, you can using the Buffer API. However in this guide, we will be using the Context API so that memory is managed automatically.
Note that Lite³ is a binary format, but the examples print message data as JSON to stdout for better readability.
As explained in the first guide, we use contexts to store Lite³ buffers (see: Context API):
We then insert the first part of the message:
Which at this point would look like this:
But now we will insert for the first time a nested object:
To make the complete message look like this:
Let's focus on the critical components here:
Almost all Lite³ functions have these two as their first parameters:
ctx): a pointer pointing to the context storing the current message that we are reading from or inserting into.ofs): the 'offset' of the target object/array.Any read or write operation on a Lite³ message must always target an object or array. This is because data will always be contained in one of those types. Recall that the JSON standard requires that the root-level always be an array or object. This also applies to Lite³, since Lite³ is JSON-compatible.
Lite³ always stores the root object/array at the start of the buffer. Therefore, if we want to access the root-level object/array, we simply pass 0 (zero-index). If we want to target a nested (internal) object, we need to pass the offset at which it is located inside the buffer.
How do we know at what offset the internal object is located?
Conveniently, whenever we insert an object, this offset is written back via the out parameter *out_ofs (the last parameter):
NULL: The new object is empty. So to target it for insertion, we pass the offset of the "headers" object as the 2nd parameter:
One nice feature of this offset mechanism is that it provides constant-time access to any internal object, regardless of nesting hierarchy. This is because no matter how deep the 'logical' document hierarchy is, at the end of the day, it will be located somewhere inside the buffer, at some offset. To find an internal object, you may have to traverse some hierarchies. But once found, the offset can be stored for later. It is then possible to target many read/write operations on this object without 'repaying' the traversing cost.
After the previous explanation, the reading example should speak for itself:
Output:
Again, the 2nd parameter is used to target the internal object. First obtain the offset, then use it to perform lookups.
We are good citizens, so we clean up after ourselves:
This destroys the context, freeing all the internal buffers so that the memory is released. When you create contexts, don't forget to destroy them, or else you will be leaking memory.
This was the fourth guide showing nested structures.
The next guide will be about arrays: Next Guide: Arrays