gRPC and Plugin support in CMake
A while back, I contributed plugin/grpc support to the CMake package that comes with protobuf. Now that I found the time, I want to write a bit about how to use this in your project to automatically generate C++ code from your protobuf files.
The full example is available in my github account: gRPC CMake example
Why?
When you build gRPC clients or servers, you will specify their API in .proto
files. These specify not only the format of the protobuf messages that get sent by gRPC, but also the services (calls) provided by the server.
Every time you update the .proto
files, you will have to run the protoc
compiler to convert them to C++ code. Actually you have to run protoc
twice: Once to generate the C++ files for the protobuf serialization and another time with the gRPC plugin to generate the server/client code.
Doing this manually is error-prone and should be avoided. This post describes how to add the .proto
files to your CMake project to automate these steps.
Structure of the project
When you look at the example project, you will see that it is split into three parts:
- A client,
- a server,
- and the proto files.
Since the protobuf/gRPC code is shared among the client and the server, I decided to put the code into a CMake library target and then link the client and server to that library. This also helps with keeping all the dependencies in one place.
In the following I will only focus on the CMakeLists.txt file in the proto/
directory.
Compiling the proto files into a library
For most of the time, you can just pretend that the .proto
files are c++ files and create a library target with them as source files:
set(PROTO_FILES
myproto/address.proto
myproto/addressbook.proto
)
# Add Library target with protobuf sources
add_library(myproto ${PROTO_FILES})
target_link_libraries(myproto
PUBLIC
protobuf::libprotobuf
gRPC::grpc
gRPC::grpc++
)
target_include_directories(myproto PUBLIC ${CMAKE_CURRENT_BINARY_DIR})
The magic happens at the very end of CMakeLists.txt
where the helper function protobuf_generate
is called. In its basic form it takes care of compiling all message types in the .proto
files to C++:
protobuf_generate(TARGET myproto LANGUAGE cpp)
This, however, will not create the gRPC client/server code, as that requires the grpc plugin. To use it, we first have to find the plugin that was installed with gRPC:
get_target_property(grpc_cpp_plugin_location gRPC::grpc_cpp_plugin LOCATION)
and once we have the location of the plugin we can tell protoc
to run it by invoking protobuf_generate
together with the PLUGIN
option:
protobuf_generate(TARGET myproto LANGUAGE grpc GENERATE_EXTENSIONS .grpc.pb.h .grpc.pb.cc PLUGIN "protoc-gen-grpc=${grpc_cpp_plugin_location}")
Note that we specified a TARGET
for protobuf_generate
. This will convert all .proto
files of that target to C++ files and will add them automatically to that target, so that they are compiled into the library.
Finding the right version of protobuf_generate
Updated/Added on 2021-09-05
Depending on your version of cmake and how you installed gRPC, you might get the following build error:
protoc-gen-grpc: program not found or is not executable
Please specify a program using absolute path or make sure the program is available in your PATH system variable
--grpc_out: protoc-gen-grpc: Plugin failed with status code 1.
Problem
So why is this happening? The reason is that there could be two versions of the protobuf cmake script installed,
which is the script that provides the protobuf_generate
command. One installed by cmake, the other installed as
part of the protobuf install.
The versions provided by cmake can lag behind (sometimes severely) the ones that come with the libraries and so,
if your cmake version isn’t up-to-date, the protobuf_generate
command might not yet support the --plugin
option,
leading to the error message you see.
This might come as a surprise, since we explicitly use find_package(protobuf CONFIG REQUIRED)
to find the protobuf
package. With the CONFIG
option, we are explicitly requesting cmake to prefer the ProtobufConfig.cmake
that comes
with protobuf, over its own (outdated) FindProtobuf.cmake
module. Unfortunately, this gets completely ignored by cmake,
because gRPC
tries to be helpful by finding all its dependencies for us, thus our find_package
for protobuf
never gets executed as gRPC
has already found the outdated one.
Solution
There are a couple of options:
Update cmake
The newer version has the improved protobuf_generate
function in it, though at the time of this writing it is already lagging behind the upstream ProtobufConfig.cmake
in other regards.
Build and install gRPC yourself
Use the following options when running cmake:
-DgRPC_PROTOBUF_PROVIDER=package
-DgRPC_PROTOBUF_PACKAGE_TYPE=CONFIG
This will instruct gRPC to find its dependencies via CONFIG
mode.
Make CONFIG mode the default
When running cmake for this example, use the -DCMAKE_FIND_PACKAGE_PREFER_CONFIG=TRUE
option to make CONFIG
mode the default mode. You then don’t have to change the way gRPC is installed. See the cmake docs for more information.