Skip to content

Flexbuffers

For flexbuffers support, you must also include the header <rfl/flexbuf.hpp> and link to the flatbuffers library. Furthermore, when compiling reflect-cpp, you need to pass -DREFLECTCPP_FLEXBUFFERS=ON to cmake. If you are using vcpkg or Conan, there should be an appropriate feature (vcpkg) or option (Conan) that will abstract this away for you.

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.