Работа с сетью в Qt

Установка и настройка protoc + gRPC-плагинов в CMake-проекте

Введение

Прежде чем писать код, нужно настроить процесс сборки — 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, а не жёстко закодированный путь к исполняемому файлу) — практика, обязательная для кросс-платформенной переносимости конфигурации, и попытка указать прямой, абсолютный путь к плагину привязывает сборку к конкретной машине разработки.