[Thiago Cafe] Programming is fun!

How to use Flatbuffers in a C++ project with Conan?

Created by Thiago Guedes on 2024-02-01 20:04:40

Tags: #c++   #flatbuffers   #conan  

In a C++ project I am currently working, we are using CMake/Conan for dependency resolution and planning to use flatbuffer to serialise some messages. When searching for documentation, I noticed that flatbuffers documentation is not the best one and that the integration with CMake is even harder to find, therefore, I decided to write a recipe on how to integrate it to reduce the misery of other developers around.

First, let me explain quickly how it works.

How flatbuffer works

  1. A fbs schema file specifies the structures of the data to be serialised
  2. flatc compiler generates a header file based on that fbs schema
  3. This header file, along with flatbuffers library will be used in your project to serialise the data

First step: Creating a very simple flatbuffer schema file

IMPORTANT: Note the root_type Frame at the end. If you don't add a root type, flatc does not generate the deserialisation functions

namespace Camera;

table Frame {
    width: uint32;
    height: uint32;
    image_data: [uint8];
}

root_type Frame;

Second step: Adding flatbuffers to conan

In the root of your project, create a file conanfile.py in the root of your project with this content:

from conan import ConanFile
from conan.tools.cmake import cmake_layout, CMakeToolchain

class ConanApplication(ConanFile):
    package_type = "application"
    settings = "os", "compiler", "build_type", "arch"
    generators = "CMakeDeps"

    def layout(self):
        cmake_layout(self)

    def generate(self):
        tc = CMakeToolchain(self)
        tc.user_presets_path = False
        tc.generate()

    def requirements(self):
        requirements = self.conan_data.get('requirements', [])
        for requirement in requirements:
            self.requires(requirement)

Now, create a file conandata.yml in the same location as conanfile.py with this content (or add the last line to your existing conandata.yml)

requirements:
  - "flatbuffers/23.5.26"

Run conan install to get the dependencies and add it to the build directory

cd ~/my-project
conan install . --output-folder=build --build=missing --settings=build_type=Debug

Third step: Changing CMakeLists.txt to parse fbs files

Before, don't forget to see the result of the conan install. It will tell you the extra parameters you need to add when running cmake generate step.

Example:

cd ~/my-project
cmake -G "Ninja" -S . -B build -DCMAKE_TOOLCHAIN_FILE=/home/thiago/src/my-project/build/build/Debug/generators/conan_toolchain.cmake -DCMAKE_POLICY_DEFAULT_CMP0091=NEW -DCMAKE_BUILD_TYPE=Debug

In your CMakeLists.txt file:

This will run flatc to compile the fbs files and generate the header files

# Use find_package to create the cmake variables of flatbuffers
find_package(flatbuffers CONFIG REQUIRED)

# flatbuffers scheme to be compiled by flac binary
set(FB_SCHEMA "${CAMNODE_FBS_DIR}/camera-frames.fbs")
# Location of the generated files. I like to use CMAKE_CURRENT_BINARY_DIR
# So it will not be in the source code tree.
set(FB_OUTDIR "${CMAKE_CURRENT_BINARY_DIR}/fbs/")

# This is an utilitary function that is available in flatbuffer cmake integration
# fbschemas is a new target that will be created with the generated file
build_flatbuffers("${FB_SCHEMA}" "" fbschemas "" "${FB_OUTDIR}" "" "")

And this will add them as a dependency of your project

# Now we create an interface library FlatbuffersTarget to add flatbuffers includes
add_library(FlatbuffersTarget INTERFACE)
target_include_directories(FlatbuffersTarget INTERFACE ${flatbuffers_INCLUDE_DIR})
add_dependencies(FlatbuffersTarget fbschemas)

# And add FB_OUTDIR to the include directories of your project's target
target_include_directories(my-project
        PUBLIC
        # ...
        ${FB_OUTDIR} # For flatbuffers generated files
)

# As well as to the project libraries
target_link_libraries(my-project PRIVATE
        # ...
        flatbuffers::flatbuffers
        FlatbuffersTarget
)

Forth step: Serialising your struct

In the application saving or sending the flatbuffer data

FlatBufferBuilder is the object that keeps the state of the serialised data. All the other builders will add data to it and mode its offset

// Include the generated header in your file
#include "camera-frames_generated.h"
// ...

// Creates a FlatBufferBuilder instance
flatbuffers::FlatBufferBuilder fbb;

// Option 1: Use a builder

// Note that I am creating a vector BEFORE creating the builder.
auto fb_vector = fbb.CreateVector(frame->image_data);
// Creates a builder using fbb instance
Camera::FrameBuilder builder(fbb);
builder.add_width(frame->width);
builder.add_height(frame->height);
builder.add_image_data(fb_vector);
// Finishes the object increasing the offset
auto fb_msg = builder.Finish();

And now, let's get the serialised data.

IMPORTANT: fbb.Finish must be called before fbb.GetBufferPointer()

// Tells the FlatBufferBuilder instance that we finished serialising our data and
// we are ready to read the buffer
fbb.Finish(fb_msg);

// Returns a pointer to the serialised data
uint8_t* buffer = fbb.GetBufferPointer();
// And the size of the array
size_t size = fbb.GetSize();

// And how do whatever you want this this data.

If you are reusing fbb, don't forget to clear it's internal state

fbb.Clear();

Fifth step: Deserialising your struct

Last but not least, it does not make sense to serialise data if this is never going to be deserialised. Thankfully it's much simpler

std::vector<uint8_t> buffer = recv_my_data(); 
auto frame = Camera::GetImageFrame(buffer.data());
// And you can use as it was a regular C++ class
logger->info("Frame received that I am ignoring for some reason: Dimensions=({}, {}), Size={}",
    frame->width(), frame->height(), dec_frame->image_data()->size());

Sixth step: Profit

Profit!

Tell me your opinion!

Reach me on Twitter - @thiedri