Skip to content

Flexbuffers

For flexbuffers support, you must also include the header <rfl/flexbuf.hpp> and link to the flatbuffers library (https://github.com/google/flatbuffers).

Flexbuffers is part of the flatbuffers library, which is a binary format developed by Google.

reflect-cpp can be used on top of flexbuffers. To see how this is advantageous, consider the following example:

Simple example

#include <rfl.hpp>
#include <rfl/flexbuf.hpp>

using Color = rfl::Literal<"Red", "Green", "Blue">;

struct Weapon {
  std::string name;
  short damage;
};

using Equipment = rfl::Variant<rfl::Field<"weapon", Weapon>>;

struct Vec3 {
  float x;
  float y;
  float z;
};

struct Monster {
  Vec3 pos;
  short mana = 150;
  short hp = 100;
  std::string name;
  bool friendly = false;
  std::vector<std::uint8_t> inventory;
  Color color = Color::make<"Blue">();
  std::vector<Weapon> weapons;
  Equipment equipped;
  std::vector<Vec3> path;
};

const auto sword = Weapon{.name = "Sword", .damage = 3};
const auto axe = Weapon{.name = "Axe", .damage = 5};

const auto weapons = std::vector<Weapon>({sword, axe});

const auto position = Vec3{1.0f, 2.0f, 3.0f};

const auto inventory =
    std::vector<std::uint8_t>({0, 1, 2, 3, 4, 5, 6, 7, 8, 9});

const auto orc = Monster{.pos = position,
                         .mana = 150,
                         .hp = 80,
                         .name = "MyMonster",
                         .inventory = inventory,
                         .color = Color::make<"Red">(),
                         .weapons = weapons,
                         .equipped = rfl::make_field<"weapon">(axe)};

const auto bytes = rfl::flexbuf::write(orc);

const auto res = rfl::flexbuf::read<Monster>(bytes);

For comparison: Standard flatbuffers

Let's talk about what normal flatbuffers would make you do to set up this example.

First of all, you would have to set up your Monster.fbs:

namespace MyGame.Sample;

enum Color:byte { Red = 0, Green, Blue = 2 }

union Equipment { Weapon } // Optionally add more tables.

struct Vec3 {
  x:float;
  y:float;
  z:float;
}

table Monster {
  pos:Vec3;
  mana:short = 150;
  hp:short = 100;
  name:string;
  friendly:bool = false (deprecated);
  inventory:[ubyte];
  color:Color = Blue;
  weapons:[Weapon];
  equipped:Equipment;
  path:[Vec3];
}

table Weapon {
  name:string;
  damage:short;
}

root_type Monster;

Then you would have to generate the C++ code using the flatbuffers compiler.

Finally, here is the code that you would then write afterwards:

// Build up a serialized buffer algorithmically:
flatbuffers::FlatBufferBuilder builder;

// First, lets serialize some weapons for the Monster: A 'sword' and an 'axe'.
auto weapon_one_name = builder.CreateString("Sword");
short weapon_one_damage = 3;

auto weapon_two_name = builder.CreateString("Axe");
short weapon_two_damage = 5;

// Use the `CreateWeapon` shortcut to create Weapons with all fields set.
auto sword = CreateWeapon(builder, weapon_one_name, weapon_one_damage);
auto axe = CreateWeapon(builder, weapon_two_name, weapon_two_damage);

// Create a FlatBuffer's `vector` from the `std::vector`.
std::vector<flatbuffers::Offset<Weapon>> weapons_vector;
weapons_vector.push_back(sword);
weapons_vector.push_back(axe);
auto weapons = builder.CreateVector(weapons_vector);

// Second, serialize the rest of the objects needed by the Monster.
auto position = Vec3(1.0f, 2.0f, 3.0f);

auto name = builder.CreateString("MyMonster");

unsigned char inv_data[] = { 0, 1, 2, 3, 4, 5, 6, 7, 8, 9 };
auto inventory = builder.CreateVector(inv_data, 10);

// Shortcut for creating monster with all fields set:
auto orc = CreateMonster(builder, &position, 150, 80, name, inventory,
                         Color_Red, weapons, Equipment_Weapon, axe.Union());

builder.Finish(orc);  // Serialize the root of the object.

I think it should be fairly obvious that using reflect-cpp on top drastically reduces the amount of boilerplate code.

But what it more, unlike "normal" flatbuffers, flexbuffers also supports field names. Field names make it a lot easier to maintain backwards compatability.

Reading and writing

Suppose you have a struct like this:

struct Person {
    std::string first_name;
    std::string last_name;
    rfl::Timestamp<"%Y-%m-%d"> birthday;
    std::vector<Person> children;
};

A person can be turned into a bytes vector like this:

const auto person = Person{...};
const auto bytes = rfl::flexbuf::write(person);

You can parse bytes like this:

const rfl::Result<Person> result = rfl::flexbuf::read<Person>(bytes);

Loading and saving

You can also load and save to disc using a very similar syntax:

const rfl::Result<Person> result = rfl::flexbuf::load<Person>("/path/to/file.fb");

const auto person = Person{...};
rfl::flexbuf::save("/path/to/file.fb", person);

Reading from and writing into streams

You can also read from and write into any std::istream and std::ostream respectively.

const rfl::Result<Person> result = rfl::flexbuf::read<Person>(my_istream);

const auto person = Person{...};
rfl::flexbuf::write(person, my_ostream);

Note that std::cout is also an ostream, so this works as well:

rfl::flexbuf::write(person, std::cout) << std::endl;

(Since flexbuffers is a binary format, the readability of this will be limited, but it might be useful for debugging).

Custom constructors

One of the great things about C++ is that it gives you control over when and how you code is compiled.

For large and complex systems of structs, it is often a good idea to split up your code into smaller compilation units. You can do so using custom constructors.

For the flexbuffers format, these must be a static function on your struct or class called from_flexbuf that take a rfl::flexbuf::Reader::InputVarType as input and return the class or the class wrapped in rfl::Result.

In your header file you can write something like this:

struct Person {
    rfl::Rename<"firstName", std::string> first_name;
    rfl::Rename<"lastName", std::string> last_name;
    rfl::Timestamp<"%Y-%m-%d"> birthday;

    using InputVarType = typename rfl::flexbuf::Reader::InputVarType;
    static rfl::Result<Person> from_flexbuf(const InputVarType& _obj);
};

And in your source file, you implement from_flexbuf as follows:

rfl::Result<Person> Person::from_flexbuf(const InputVarType& _obj) {
    const auto from_nt = [](auto&& _nt) {
        return rfl::from_named_tuple<Person>(std::move(_nt));
    };
    return rfl::flexbuf::read<rfl::named_tuple_t<Person>>(_obj)
        .transform(from_nt);
}

This will force the compiler to only compile the flexbuffers parsing when the source file is compiled.