File: flexbuffers.md

package info (click to toggle)
reflect-cpp 0.18.0%2Bds-3
  • links: PTS, VCS
  • area: main
  • in suites: trixie
  • size: 12,524 kB
  • sloc: cpp: 44,484; python: 131; makefile: 30; sh: 3
file content (248 lines) | stat: -rw-r--r-- 7,033 bytes parent folder | download | duplicates (2)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
# Flexbuffers

For flexbuffers support, you must also include the header `<rfl/flexbuf.hpp>` and link to the [flatbuffers](https://github.com/google/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

```cpp
#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:

```cpp
// 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:

```cpp
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:

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

You can parse bytes like this:

```cpp
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:

```cpp
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.

```cpp
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:

```cpp
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:

```cpp
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:

```cpp
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.