[C++] Protobuf: Project Setup With Conan and CMake

In my last article I wrote about Conan, a C/C++ package manager. In this article we'll continue using Conan and start a simple project with Google Protocolbuffers.

Please note: This post is about setting up a clean protobuffer project with Conan and CMake.

Before we go through this project, we'll add Bincrafters to the conan remotes to download Google Protocolbuffers. The project structure will look like the following tree (without the build directory):

.
├── CMakeLists.txt
├── conanfile.txt
├── proto
│   └── Animal.proto
└── src
    └── main.cpp
 

Bincrafters

Bincrafters is another remote, where we access Google Protocolbuffers. Run the following two commands to add this remote. We need to set revisions to 1, refer to this article for further information.

conan remote add bincrafters https://bincrafters.jfrog.io/artifactory/api/conan/public-conan
conan config set general.revisions_enabled=1
 

conanfile.txt

We need two dependencies,

  1. the protobuffer compiler to create the generic classes which we use in our C++ code and
  2. the protobuffer libraries to link our code.

The conanfile.txt contains then:

[requires]
protobuf/3.9.1@bincrafters/stable
protoc_installer/3.9.1@bincrafters/stable

[generators]
cmake

Now we can run conan to download the dependencies and create the conanbuildinfo.cmake. Create a build directory and run conan (and build all missing packages).

mkdir build
cd build 
conan install .. --build missing
 

proto/Animal.proto

This defines the proto message, which we want to use. We simply define an animal with three fields: Species, name and age. The protobuffer compiler will create an animal class which we'll use in src/main.cpp. We are using latest syntax version 3, for more information and a syntax guide visit the developers.google

// ./proto/Animal.proto 
// protobuffer framework will generate the classes which we'll access later

syntax = "proto3";

message Animal {
  string species = 1;
  string name = 2;
  int32 age = 3;
}
 

CMakeLists.txt

At first we need to include the conanbuildinfo.cmake and run conan_basic_setup(). Then we can find the protobuffer package. By adding the Animal.proto to the target executable, we can run the protobuf_generate macro to create the generic animal class.

cmake_minimum_required(VERSION 3.16)
project(Protobuffer)

set(target ProtobufferExample)


include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
conan_basic_setup()
 

# we'll find the protobuffer dependency which was downloaded with conan
find_package(Protobuf REQUIRED)

# add the Animal.proto here to create the generic classes with protobuf_generate(...)
add_executable(${target}
  ${PROJECT_SOURCE_DIR}/proto/Animal.proto
  ${PROJECT_SOURCE_DIR}/src/main.cpp
)
 
protobuf_generate(TARGET ${target}
  PROTOS ${PROJECT_SOURCE_DIR}/proto/Animal.proto    
)
 
target_link_libraries(${target}
  ${CONAN_LIBS}
)

# the output directory for the proto compiler is our build directory
# if we want to access the generic animal header we need to include this directory
target_include_directories(${target} PUBLIC
  ${CMAKE_CURRENT_BINARY_DIR}
)

Now run the CMake configure command (make sure that the current working directory is still ./build):

cmake -S ./.. -B ./

The created animal class is in the build directory.

.
├── build
│   └── proto
│       ├── Animal.pb.cc
│       └── Animal.pb.h
 

src/main.cpp

We just craete one Animal and call their setters and getters to demonstrate that the project was successful built and we can use protobuf library.

#include <iostream>

#include "proto/Animal.pb.h"
 
int main(int argc, char* argv[])
{
    Animal message;
    message.set_species("Cat");
    message.set_name("Tiffany");
    message.set_age(12);

    std::cout << "created proto message with animal:\n"
        << "species: " << message.species() << '\n'
        << "name: " << message.name() << '\n'
        << "age: " << message.age() << '\n';

    return 0;
}
 

Build And Run

Now use CMake to finally build the project and execute it. We are now able to use google protocolbuffers in this project.

cmake --build ./
./bin/ProtobufferExample
 

Conclusion

Once more Conan has proven to be a good solution for managing dependencies. A few advantages for me are:

  1. Conan downloads and builds protobuf and the proto compiler
  2. The proto compiler is executed by cmake
  3. A simple CMakeLists.txt with just a few lines of code

The project is on GitHub. Feel free to play around.

That's it for now.

Best Thomas

 

Edit: Remove find_package(..) From CMakeLists.txt

We can get rid of the find_package(Protobuf REQUIRED) macro in the CMakeLists.txt, by adding cmake_paths to the [generators] in the conanfile.txt.

[requires]
protobuf/3.9.1@bincrafters/stable
protoc_installer/3.9.1@bincrafters/stable

[generators]
cmake
cmake_paths

And then we include the new created conan_paths.cmake and protoc-config.cmake in the CMakeLists.txt. As result we can drop the find_package(Protobuf REQUIRED) command.

cmake_minimum_required(VERSION 3.16)
project(Protobuffer)

set(target ProtobufferExample)


include(${CMAKE_BINARY_DIR}/conanbuildinfo.cmake)
# EDIT: after adding cmake_paths in conanfile.txt we can include conan_paths.cmake
include(${CMAKE_BINARY_DIR}/conan_paths.cmake)
conan_basic_setup()
 

# find_package(Protobuf REQUIRED)
# EDIT: and we can get rid of the find_package command by including protoc-config.cmake
include(${CONAN_PROTOC_INSTALLER_ROOT}/lib/cmake/protoc/protoc-config.cmake)

add_executable(${target}
  ${PROJECT_SOURCE_DIR}/proto/Animal.proto
  ${PROJECT_SOURCE_DIR}/src/main.cpp
)
 
protobuf_generate(TARGET ${target}
  PROTOS ${PROJECT_SOURCE_DIR}/proto/Animal.proto    
)
 
target_link_libraries(${target}
  ${CONAN_LIBS}
)

target_include_directories(${target} PUBLIC
  ${CMAKE_CURRENT_BINARY_DIR}
)

Edit 2022:

As Tom from the comments pointed out, protobuf_generate stopped working. I noticed this as well a while ago and as quick fix you can simply use cmake's execute_process to use the proto compiler. You also can create a cmake function/macro to use it.

# unfortunately the directory will not be created by protoc
file(MAKE_DIRECTORY ${CMAKE_BINARY_DIR}/proto)

# execute protoc from the conan directory
# pass the filename of the proto file
# I use by default the build directory to put the generated files in
# and set the working directory where your proto file is located
execute_process(
    COMMAND 
        "${CONAN_BIN_DIRS_PROTOBUF}/protoc" 
        "my_protofile.proto"  
        "--cpp_out=${CMAKE_BINARY_DIR}/proto" 
    WORKING_DIRECTORY "${DIRECTORY WHERE my_protofile.proto IS LOCATED}"
)

add_executable(${your target}  
    ${CMAKE_BINARY_DIR}/proto/my_protofile.pb.cc
    # ...
)

# ...
# later on don't forget to set the include directory to use your proto class
target_include_directories(${target} PUBLIC 
    ${CMAKE_BINARY_DIR}/proto
    # ...
)
Previous
Previous

Use Frameworks But Don’t Marry Them

Next
Next

[C++] Getting Started With Conan