Finding Protobuf
Protocol Buffers (protobuf) is Google's language-neutral, platform-neutral serialization mechanism. CMake integration involves two components: the protoc compiler (for code generation) and the runtime library (for serialization/deserialization at runtime).
# CMakeLists.txt — Finding Protobuf
cmake_minimum_required(VERSION 3.20)
project(MyService LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
# Find protobuf — provides protoc compiler and runtime library
find_package(Protobuf REQUIRED)
message(STATUS "Protobuf version: ${Protobuf_VERSION}")
message(STATUS "Protobuf include: ${Protobuf_INCLUDE_DIRS}")
message(STATUS "Protobuf libraries: ${Protobuf_LIBRARIES}")
message(STATUS "Protoc compiler: ${Protobuf_PROTOC_EXECUTABLE}")
# Install protobuf on various platforms
# Ubuntu/Debian
sudo apt install libprotobuf-dev protobuf-compiler
# macOS
brew install protobuf
# vcpkg (cross-platform)
vcpkg install protobuf protobuf[zlib]
# Conan
conan install protobuf/25.3@
protoc compiler version must match the runtime library version exactly. Mismatched versions cause subtle serialization bugs. Use find_package(Protobuf 25.3 EXACT REQUIRED) for strict version pinning.
protobuf_generate_cpp()
CMake's protobuf_generate_cpp() function invokes protoc to generate C++ source and header files from .proto definitions:
# Proto file code generation
find_package(Protobuf REQUIRED)
# Generate C++ from .proto files
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS
proto/messages.proto
proto/enums.proto
proto/services.proto
)
# Create library from generated code + your implementation
add_library(my_proto_lib ${PROTO_SRCS} ${PROTO_HDRS})
target_link_libraries(my_proto_lib PUBLIC protobuf::libprotobuf)
target_include_directories(my_proto_lib PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
# Main application links against proto library
add_executable(myapp src/main.cpp)
target_link_libraries(myapp PRIVATE my_proto_lib)
# Modern approach using protobuf_generate (CMake 3.21+)
find_package(Protobuf REQUIRED)
add_library(my_proto_lib)
target_link_libraries(my_proto_lib PUBLIC protobuf::libprotobuf)
target_include_directories(my_proto_lib PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
# protobuf_generate attaches generated sources to the target
protobuf_generate(
TARGET my_proto_lib
PROTOS proto/messages.proto proto/enums.proto
IMPORT_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/proto
)
gRPC Integration
gRPC extends protobuf with RPC service definitions. The CMake integration requires both the protobuf and gRPC code generators:
# CMakeLists.txt — gRPC + Protobuf
cmake_minimum_required(VERSION 3.20)
project(MyGrpcService LANGUAGES CXX)
set(CMAKE_CXX_STANDARD 17)
find_package(Protobuf REQUIRED)
find_package(gRPC REQUIRED)
# Get the grpc_cpp_plugin path
get_target_property(GRPC_CPP_PLUGIN gRPC::grpc_cpp_plugin LOCATION)
# Proto file
set(PROTO_FILE ${CMAKE_CURRENT_SOURCE_DIR}/proto/greeter.proto)
# Generate protobuf C++ code
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${PROTO_FILE})
# Generate gRPC C++ code (service stubs)
set(GRPC_SRCS "${CMAKE_CURRENT_BINARY_DIR}/greeter.grpc.pb.cc")
set(GRPC_HDRS "${CMAKE_CURRENT_BINARY_DIR}/greeter.grpc.pb.h")
add_custom_command(
OUTPUT ${GRPC_SRCS} ${GRPC_HDRS}
COMMAND protobuf::protoc
ARGS --grpc_out=${CMAKE_CURRENT_BINARY_DIR}
--plugin=protoc-gen-grpc=${GRPC_CPP_PLUGIN}
-I ${CMAKE_CURRENT_SOURCE_DIR}/proto
${PROTO_FILE}
DEPENDS ${PROTO_FILE}
COMMENT "Generating gRPC C++ stubs"
)
# Create proto library
add_library(greeter_proto
${PROTO_SRCS} ${PROTO_HDRS}
${GRPC_SRCS} ${GRPC_HDRS}
)
target_link_libraries(greeter_proto PUBLIC
protobuf::libprotobuf
gRPC::grpc++
gRPC::grpc++_reflection
)
target_include_directories(greeter_proto PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
# Server executable
add_executable(server src/server.cpp)
target_link_libraries(server PRIVATE greeter_proto)
# Client executable
add_executable(client src/client.cpp)
target_link_libraries(client PRIVATE greeter_proto)
Custom protoc Plugins
You can extend protoc with custom code generators for validation layers, documentation, or alternative serialization formats:
# Using a custom protoc plugin
find_package(Protobuf REQUIRED)
# Find or build the custom plugin
find_program(PROTO_VALIDATOR_PLUGIN protoc-gen-validate)
set(PROTO_FILE ${CMAKE_CURRENT_SOURCE_DIR}/proto/user.proto)
# Generate standard C++ code
protobuf_generate_cpp(PROTO_SRCS PROTO_HDRS ${PROTO_FILE})
# Generate validation code with custom plugin
set(VALIDATE_SRCS "${CMAKE_CURRENT_BINARY_DIR}/user.pb.validate.cc")
set(VALIDATE_HDRS "${CMAKE_CURRENT_BINARY_DIR}/user.pb.validate.h")
add_custom_command(
OUTPUT ${VALIDATE_SRCS} ${VALIDATE_HDRS}
COMMAND protobuf::protoc
ARGS --validate_out="lang=cc:${CMAKE_CURRENT_BINARY_DIR}"
--plugin=protoc-gen-validate=${PROTO_VALIDATOR_PLUGIN}
-I ${CMAKE_CURRENT_SOURCE_DIR}/proto
-I ${Protobuf_INCLUDE_DIRS}
${PROTO_FILE}
DEPENDS ${PROTO_FILE}
COMMENT "Generating validation code"
)
add_library(user_proto
${PROTO_SRCS} ${PROTO_HDRS}
${VALIDATE_SRCS} ${VALIDATE_HDRS}
)
target_link_libraries(user_proto PUBLIC protobuf::libprotobuf)
Generated Source Management
Managing generated protobuf sources cleanly is critical for maintainable builds. The generated files live in the binary directory and must be properly tracked:
# Clean generated source management pattern
set(PROTO_DIR ${CMAKE_CURRENT_SOURCE_DIR}/proto)
set(PROTO_OUT ${CMAKE_CURRENT_BINARY_DIR}/generated)
file(MAKE_DIRECTORY ${PROTO_OUT})
# Collect all .proto files
file(GLOB PROTO_FILES "${PROTO_DIR}/*.proto")
# Generate all at once with PROTOBUF_GENERATE_CPP
set(ALL_PROTO_SRCS "")
set(ALL_PROTO_HDRS "")
foreach(PROTO_FILE ${PROTO_FILES})
get_filename_component(PROTO_NAME ${PROTO_FILE} NAME_WE)
list(APPEND ALL_PROTO_SRCS "${PROTO_OUT}/${PROTO_NAME}.pb.cc")
list(APPEND ALL_PROTO_HDRS "${PROTO_OUT}/${PROTO_NAME}.pb.h")
endforeach()
add_custom_command(
OUTPUT ${ALL_PROTO_SRCS} ${ALL_PROTO_HDRS}
COMMAND protobuf::protoc
ARGS --cpp_out=${PROTO_OUT}
-I ${PROTO_DIR}
${PROTO_FILES}
DEPENDS ${PROTO_FILES}
COMMENT "Generating protobuf C++ sources"
)
# Mark generated sources so CMake knows they'll appear at build time
set_source_files_properties(
${ALL_PROTO_SRCS} ${ALL_PROTO_HDRS}
PROPERTIES GENERATED TRUE
)
add_library(proto_lib ${ALL_PROTO_SRCS} ${ALL_PROTO_HDRS})
target_link_libraries(proto_lib PUBLIC protobuf::libprotobuf)
target_include_directories(proto_lib PUBLIC ${PROTO_OUT})
file(GLOB ...) for proto files without CONFIGURE_DEPENDS — new proto files won't be detected until you manually re-run CMake. For production builds, list proto files explicitly.
Arena Allocation Config
Protobuf's arena allocation reduces heap fragmentation for message-heavy workloads. Configure it through CMake compile definitions:
# Enable arena allocation optimizations
add_library(proto_lib ${PROTO_SRCS} ${PROTO_HDRS})
target_link_libraries(proto_lib PUBLIC protobuf::libprotobuf)
# Enable arena support in generated code
target_compile_definitions(proto_lib PUBLIC
GOOGLE_PROTOBUF_NO_RTTI=1 # Smaller binaries if RTTI unused
)
# For proto3 arena optimization, use option in .proto file:
# option cc_enable_arenas = true;
// Using arena allocation in application code
#include <google/protobuf/arena.h>
#include "messages.pb.h"
void process_batch() {
google::protobuf::Arena arena;
// Allocate messages on the arena (bulk deallocation)
auto* request = google::protobuf::Arena::CreateMessage<Request>(&arena);
request->set_id(42);
request->set_payload("data");
auto* response = google::protobuf::Arena::CreateMessage<Response>(&arena);
// ... process ...
// All messages freed when arena goes out of scope — one deallocation
}
Cross-Language Builds
Protobuf's strength is cross-language compatibility. A single CMake project can generate code for C++, Python, and other languages simultaneously:
# Multi-language proto generation
find_package(Protobuf REQUIRED)
set(PROTO_DIR ${CMAKE_CURRENT_SOURCE_DIR}/proto)
set(PROTO_FILES
${PROTO_DIR}/messages.proto
${PROTO_DIR}/services.proto
)
# C++ generation
set(CPP_OUT ${CMAKE_CURRENT_BINARY_DIR}/cpp)
file(MAKE_DIRECTORY ${CPP_OUT})
protobuf_generate_cpp(CPP_SRCS CPP_HDRS ${PROTO_FILES})
# Python generation
set(PY_OUT ${CMAKE_CURRENT_BINARY_DIR}/python)
file(MAKE_DIRECTORY ${PY_OUT})
add_custom_command(
OUTPUT ${PY_OUT}/messages_pb2.py ${PY_OUT}/services_pb2.py
COMMAND protobuf::protoc
ARGS --python_out=${PY_OUT}
-I ${PROTO_DIR}
${PROTO_FILES}
DEPENDS ${PROTO_FILES}
COMMENT "Generating Python protobuf code"
)
add_custom_target(proto_python ALL
DEPENDS ${PY_OUT}/messages_pb2.py ${PY_OUT}/services_pb2.py
)
# C++ library
add_library(proto_cpp ${CPP_SRCS} ${CPP_HDRS})
target_link_libraries(proto_cpp PUBLIC protobuf::libprotobuf)
target_include_directories(proto_cpp PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
Proto Import Paths
Complex projects with multiple proto directories require careful import path configuration to resolve cross-file references:
# Managing proto import paths for a monorepo
# Project structure:
# proto/
# common/types.proto
# services/user.proto (imports common/types.proto)
# services/order.proto (imports common/types.proto)
set(PROTO_ROOT ${CMAKE_CURRENT_SOURCE_DIR}/proto)
# Import paths (-I flags to protoc)
set(PROTO_IMPORT_DIRS
${PROTO_ROOT} # Project protos
${Protobuf_INCLUDE_DIRS} # Well-known types (google/protobuf/*)
${CMAKE_CURRENT_SOURCE_DIR}/third_party # Vendored protos
)
# Build import path string
set(PROTO_IMPORT_FLAGS "")
foreach(DIR ${PROTO_IMPORT_DIRS})
list(APPEND PROTO_IMPORT_FLAGS "-I${DIR}")
endforeach()
# Generate with all import paths
add_custom_command(
OUTPUT ${GENERATED_SRCS}
COMMAND protobuf::protoc
ARGS --cpp_out=${CMAKE_CURRENT_BINARY_DIR}
${PROTO_IMPORT_FLAGS}
${PROTO_FILES}
DEPENDS ${PROTO_FILES}
COMMENT "Generating protobuf with import paths"
)
package mycompany.services.v1; in proto/mycompany/services/v1/). This prevents import conflicts across teams and makes the import paths intuitive.