Cap'n Proto¶
For Cap'n Proto support, you must also include the header <rfl/capnproto.hpp>
and link to the capnproto library.
Furthermore, when compiling reflect-cpp, you need to pass -DREFLECTCPP_CAPNPROTO=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.
Cap'n Proto is a schemaful binary format. This sets it apart from most other formats supported by reflect-cpp, which are schemaless.
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
struct can be serialized to a bytes vector like this:
const auto person = Person{...};
const std::vector<char> bytes = rfl::capnproto::write(person);
You can parse bytes like this:
const rfl::Result<Person> result = rfl::capnproto::read<Person>(bytes);
The schema¶
However, Cap'n Proto is a schemaful format, so before you serialize or deserialize, you have to declare a schema. In the two function calls above, this is abstracted away.
If you want to, you can pass the schema explicitly, but it will not yield any performance gains, because the schemata are always created upfront:
const auto schema = rfl::capnproto::to_schema<Person>();
const auto person = Person{...};
const std::vector<char> bytes = rfl::capnproto::write(person, schema);
const rfl::Result<Person> result = rfl::capnproto::read<Person>(bytes, schema);
Cap'n Proto schemas are created using a schema language. You can retrieve the schema like this:
schema.str();
In this case, the resulting schema representation looks like this:
@0xdbb9ad1f14bf0b36;
struct Person {
firstName @0 :Text;
lastName @1 :Text;
birthday @2 :Text;
children @3 :List(Person);
}
Loading and saving¶
You can also load and save to disc using a very similar syntax:
const rfl::Result<Person> result = rfl::capnproto::load<Person>("/path/to/file.capnproto");
const auto person = Person{...};
rfl::capnproto::save("/path/to/file.capnproto", 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::capnproto::read<Person>(my_istream);
const auto person = Person{...};
rfl::capnproto::write(person, my_ostream);
Note that std::cout
is also an ostream, so this works as well:
rfl::capnproto::write(person, std::cout) << std::endl;
(Since Cap'n Proto 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 Cap'n Proto format, these must be a static function on your struct or class called
from_capnproto
that take a rfl::capnproto::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::capnproto::Reader::InputVarType;
static rfl::Result<Person> from_capnproto(const InputVarType& _obj);
};
And in your source file, you implement from_capnproto
as follows:
rfl::Result<Person> Person::from_capnproto(const InputVarType& _obj) {
const auto from_nt = [](auto&& _nt) {
return rfl::from_named_tuple<Person>(std::move(_nt));
};
return rfl::capnproto::read<rfl::named_tuple_t<Person>>(_obj)
.transform(from_nt);
}
This will force the compiler to only compile the Cap'n Proto parsing when the source file is compiled.