Введение
Прежде чем писать код, нужно настроить процесс сборки — protoc (компилятор Protobuf) и плагин grpc_cpp_plugin должны автоматически генерировать C++ код из .proto-файлов как часть обычной сборки CMake-проекта, без необходимости запускать эти инструменты вручную при каждом изменении контракта. В статье разберём настройку этого процесса, продолжая подход к управлению зависимостями из основного цикла статей про CMake.
Концепция
Современный gRPC поставляется с собственными CMake-модулями (find_package(gRPC), find_package(Protobuf)), и большинство современных систем управления пакетами (vcpkg, Conan, упомянутые в основном цикле статей, статьи 33, 40) предоставляют готовые пакеты gRPC, избавляя от необходимости собирать его из исходников самостоятельно. Генерация кода из .proto-файлов автоматизируется через CMake-функции protobuf_generate()/специальные custom-команды, вызывающие protoc с нужными плагинами на этапе конфигурации или сборки — сгенерированные .pb.cc/.pb.h (структуры данных) и .grpc.pb.cc/.grpc.pb.h (сервисные классы) файлы затем компилируются и линкуются как обычная часть проекта.
Пример кода: Установка и настройка
# CMakeLists.txt — настройка gRPC через vcpkg
cmake_minimum_required(VERSION 3.20)
project(TelemetryGrpcApp)
set(CMAKE_CXX_STANDARD 17)
find_package(Protobuf CONFIG REQUIRED)
find_package(gRPC CONFIG REQUIRED)
find_package(Qt6 REQUIRED COMPONENTS Core Network)
# Путь к .proto файлу, общему для клиента и сервера
set(PROTO_FILE ${CMAKE_SOURCE_DIR}/proto/device_service.proto)
# Директория для сгенерированных файлов — отдельно от исходников проекта,
# чтобы не путать вручную написанный код с автоматически сгенерированным
set(GENERATED_DIR ${CMAKE_BINARY_DIR}/generated)
file(MAKE_DIRECTORY ${GENERATED_DIR})
# Генерация кода сообщений Protobuf (.pb.cc / .pb.h)
add_custom_command(
OUTPUT ${GENERATED_DIR}/device_service.pb.cc ${GENERATED_DIR}/device_service.pb.h
COMMAND protobuf::protoc
ARGS --cpp_out=${GENERATED_DIR}
-I ${CMAKE_SOURCE_DIR}/proto
${PROTO_FILE}
DEPENDS ${PROTO_FILE}
COMMENT "Генерация Protobuf-кода сообщений"
)
# Генерация кода gRPC-сервиса (.grpc.pb.cc / .grpc.pb.h) — отдельный плагин protoc
add_custom_command(
OUTPUT ${GENERATED_DIR}/device_service.grpc.pb.cc ${GENERATED_DIR}/device_service.grpc.pb.h
COMMAND protobuf::protoc
ARGS --grpc_out=${GENERATED_DIR}
--plugin=protoc-gen-grpc=$<TARGET_FILE:gRPC::grpc_cpp_plugin>
-I ${CMAKE_SOURCE_DIR}/proto
${PROTO_FILE}
DEPENDS ${PROTO_FILE}
COMMENT "Генерация gRPC-кода сервиса"
)
add_executable(TelemetryClient
main.cpp
${GENERATED_DIR}/device_service.pb.cc
${GENERATED_DIR}/device_service.grpc.pb.cc
)
target_include_directories(TelemetryClient PRIVATE ${GENERATED_DIR})
target_link_libraries(TelemetryClient PRIVATE
Qt6::Core
Qt6::Network
gRPC::grpc++
protobuf::libprotobuf
)
# Установка зависимостей через vcpkg — самый простой путь для Windows-разработки
vcpkg install grpc protobuf
cmake -B build -S . -DCMAKE_TOOLCHAIN_FILE=[путь_к_vcpkg]/scripts/buildsystems/vcpkg.cmake
cmake --build build
Пояснения к коду
Два отдельных вызова add_custom_command() отвечают за две разные части генерируемого кода — первый вызывает обычный protoc без дополнительного плагина, генерируя только классы данных сообщений (SensorReading, ReadingRequest из статьи 321), а второй явно подключает --plugin=protoc-gen-grpc=..., генерирующий дополнительный код, специфичный именно для gRPC-сервисов (клиентские stub-классы и серверные базовые классы для реализации) — это разделение отражает то, что Protobuf и gRPC технически отдельные, хотя тесно интегрированные между собой инструменты: можно использовать Protobuf для сериализации данных без gRPC вовсе (например, просто сохраняя сообщения в файл), но не наоборот.
DEPENDS ${PROTO_FILE} гарантирует, что CMake перегенерирует код автоматически при изменении самого .proto-файла, без необходимости вручную удалять старые сгенерированные файлы или запускать protoc отдельно — это поддерживает основной принцип gRPC-разработки: .proto-файл — единственный источник истины, а весь производный код пересобирается автоматически при каждом изменении контракта.
Подводные камни
- Несовпадение версий
protocи сгенерированных библиотек gRPC/Protobuf, особенно при смешивании пакетов из разных источников (системный пакетный менеджер плюс vcpkg, или разные версии vcpkg-портов на разных машинах команды) — это одна из самых частых причин трудноуловимых ошибок компиляции или линковки в gRPC-проектах, и привязка к единому, явно зафиксированному менеджеру пакетов (vcpkg/Conan с зафиксированными версиями, основной цикл статей, статья 40) существенно снижает этот риск. - Отсутствие явной директории для сгенерированных файлов, отдельной от исходников проекта, без которой автоматически сгенерированный код легко перепутать с вручную написанным и случайно закоммитить в систему контроля версий или попытаться отредактировать вручную (что бессмысленно, поскольку при следующей генерации правки будут потеряны) — явное разделение
GENERATED_DIRот обычной структуры исходников, как в примере, и явное добавление этой директории в.gitignore, помогает избежать этой путаницы. - Длительное время первой сборки проекта с gRPC, особенно при сборке самого gRPC из исходников (а не через готовый пакет vcpkg/Conan) — gRPC имеет довольно много транзитивных зависимостей (включая собственную копию Protobuf, абсолютно необходимую c-ares для DNS-резолвинга, и другие), и полная сборка с нуля может занимать значительно больше времени, чем сборка простого Qt-приложения без таких зависимостей; использование готовых, предсобранных пакетов package-менеджера почти всегда предпочтительнее самостоятельной сборки gRPC с нуля для большинства практических случаев.
- Кросс-платформенные различия в путях и именах исполняемых файлов
protoc/grpc_cpp_plugin, особенно при переносе CMake-конфигурации между Windows и Linux/macOS — использование$<TARGET_FILE:gRPC::grpc_cpp_plugin>(генератор выражений CMake, а не жёстко закодированный путь к исполняемому файлу) — практика, обязательная для кросс-платформенной переносимости конфигурации, и попытка указать прямой, абсолютный путь к плагину привязывает сборку к конкретной машине разработки.