Serialization in C
Serialization infrastructure in C is very basic and is currently limited to built-in types and big-endian protocols.
To (de)serialize a buffer, follow these steps:
- Create a serializer and initialize it via microstrain_serializer_init_insertion or microstrain_serializer_init_extraction, depending on whether you're writing or reading data.
- Call
microstrain_insert_*
or microstrain_extract_*
for each parameter. E.g. microstrain_extract_u32.
- Call microstrain_serializer_is_ok to check if all the data was written/read successfully (i.e. fit in the buffer). Alternatively, to verify if exactly buffer_size bytes were read/written, use microstrain_serializer_is_complete.
- Transmit the written buffer or use the deserialized parameters.
When reading an array length from a buffer, it is recommended to use microstrain_extract_count to specify a maximum length. This helps avoid buffer overrun bugs and associated security vulnerabilities.
Serialization in C++
The MIP SDK includes a complete serialization system in C++. It supports both big- and little-endian buffers and user- defined types.
Basic usage
To (de)serialize a buffer, follow these steps:
- Create a microstrain::Serializer, passing in a pointer to your buffer and the size. A starting offset may also be specified for convenience.
- Call microstrain::Serializer::insert or microstrain::Serializer.extract with the values to be (de)serialized. Multiple calls may be made to these functions if needed. When reading an array length from a buffer, it is recommended to use microstrain::Serializer::extract_count to specify a maximum count. This helps avoid buffer overrun bugs and associated security vulnerabilities.
- Check if the data was written/read successfully (i.e. fit in the buffer) by calling microstrain::Serializer::isOk or microstrain::Serializer::isFinished (use the latter if the entire buffer should have been used).
- Transmit the written buffer or use the deserialized parameters.
Example:
int main()
{
uint8_t buffer_le[128];
uint8_t buffer_be[128];
uint8_t a = 22;
int8_t b = -33;
uint16_t c = 1024;
int32_t d = -1000000;
uint64_t e = 0x81726354AABBCCDD;
float f = 1.25f;
double g = -1.1;
bes.insert(a,b,c,d,e,f,g);
les.insert(a,b,c,d,e,f,g);
uint64_t too_much[20] = {0};
bes.insert(too_much);
les.insert(&too_much[0], 20);
assert(!bes.isOk() && !les.isOk());
bes.setOffset(0);
les.setOffset(0);
assert(bes.isOk() && les.isOk());
bes.extract(a,b,c,d,e,f,g);
les.extract(a,b,c,d,e,f,g);
if(!bes.isOk() || !les.isOk())
return 1;
bes.setOffset(4);
les.setOffset(4);
std::optional vg = microstrain::extract<int32_t>(bes);
assert( vg.has_value() && *vg == d );
bes.setOffset(0);
les.setOffset(0);
return 0;
}
Supported Types
The serialization library has support for the following basic types:
- Booleans (as a u8; false->[0x00], true->[0x01]; 0x00 reads as false, anything else as true)
- Signed and unsigned integers of various sizes (u8, s8, u16, ..., u64, s64)
- Floating point values (float and double)
- Enums, provided they have an underlying type specified
Additionally, the following compound types are supported:
Serialization "One-liners"
For convenience, a few additional methods are provided for serialization in a single line.
- insert to raw buffer
- extract from raw buffer
- extract to std::optional
void one_liners()
{
uint8_t buffer[4];
int32_t x = -501;
bool ok1 = microstrain::insert<microstrain::Endian::big>(x, span);
bool ok2 = microstrain::insert<microstrain::Endian::big>(x, buffer, sizeof(buffer));
bool ok3 = microstrain::insert<microstrain::Endian::big>(x, buffer, sizeof(buffer), 0, true);
assert(ok1 && ok2 && ok3);
int32_t y1,y2,y3;
ok1 = microstrain::extract<microstrain::Endian::big>(y1, span);
ok2 = microstrain::extract<microstrain::Endian::big>(y2, span, 0, true);
ok3 = microstrain::extract<microstrain::Endian::big>(y3, buffer, sizeof(buffer));
assert(ok1 && ok2 && ok3 && y1 == x && y2 == x && y3 == x);
std::optional<float> value1 = microstrain::extract<microstrain::Endian::big, int32_t>(span);
std::optional<float> value2 = microstrain::extract<microstrain::Endian::big, int32_t>(buffer, sizeof(buffer));
assert(value1.has_value() && value2.has_value());
assert(*value1 == x && *value2 == x);
}
User-defined types
Classes and structs
Classes and structs may include one or more of the following member functions to enable serialization support:
void insert(microstrain::BigEndianSerializer& serializer) const
void insert(microstrain::LittleEndianSerializer& serializer) const
template<microstrain::Endian E> void insert(microstrain::Serializer<E>& serializer) const
void extract(microstrain::BigEndianSerializer& serializer)
void extract(microstrain::LittleEndianSerializer& serializer)
template<microstrain::Endian E> void extract(microstrain::Serializer<E>& serializer)
In addition, the serialization non-member functions may be overloaded as described next.
Other user-defined types
All serialization goes through microstrain::insert
/ microstrain::extract
. These are non-member functions and are overloaded for various data types. This makes it possible to extend serialization to new types. Serialization for any custom type may be implemented by overloading the insert
or extract
functions. For example:
namespace custom
{
enum Foo { A=0, B, C, MAX_FOO };
template<microstrain::Endian E>
{
}
template<microstrain::Endian E>
{
uint8_t value;
if(serializer.isOk())
{
if(value < MAX_FOO)
foo = value;
else
}
return size;
}
}
void write_read_foo(custom::Foo foo)
{
uint8_t buffer[8];
custom::Foo foo2;
assert(foo2 == foo);
}
Serialization System Architecture
The MIP library implements many custom types and heavily leverages the serialization system. It uses insert
/extract
overloads, class methods, strongly-typed enums, and arrays.
The Serializer uses read/write functions from the microstrain::serialization namespace to handle endianness / byteswapping and packing at the lowest level.
This diagram describes the architecture of the serialization system: