Table of Contents

  1. Finding Protobuf
  2. protobuf_generate_cpp()
  3. gRPC Integration
  4. Custom protoc Plugins
  5. Generated Source Management
  6. Arena Allocation Config
  7. Cross-Language Builds
  8. Proto Import Paths
Back to CMake Mastery Series

Protocol Buffers

June 4, 2026 Wasil Zafar 10 min read

The definitive guide to integrating Protocol Buffers and gRPC with CMake — code generation, custom protoc plugins, import path management, and multi-language builds.

Serialization

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@
Key Insight: The 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})
Pitfall: Never use 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"
)
Best Practice: Structure your proto files with a clear namespace hierarchy matching directory structure (e.g., package mycompany.services.v1; in proto/mycompany/services/v1/). This prevents import conflicts across teams and makes the import paths intuitive.