Home How to create a bi-directional gRPC client in C++
Post
Cancel

How to create a bi-directional gRPC client in C++

In the previous article on gRPC I described creating a client and server in go that could stream messages using gRPC bi-directional streaming. The client in that article was just for testing purposes - not something I can deploy on one of my robots. The purpose of this article is to get closer to something that can run on actual robot hardware. We need to use the service description in protobuf format to generate stub code and then build a complete client. We then need to update the GitHub Actions already in place to automate the build.

You can find all the code discussed here in this pull request.

Why C++?

You might be wondering why build a client in C++ given I have a working client already in go. The first robot I’m targetting is NAO which only has python and C++ SDKs. The NAO python is easy to use, but only supports python 2.7 and I’d like to make use of recent libraries. There is actually a Java SDK but it’s buggy, incomplete, little used and can’t easily be used to run code hosted on the robot. My hope is that with C++ I can build something reasonably efficient and still make use of modern libraries and frameworks by building them from source.

The Service

Here is the very basic service we need a client for:

1
2
3
service TheSocialRobot {
    rpc EventStream(stream BodyEvent) returns (stream BodyCommand) {}
}

It’s currently very simple: the robot sends a stream of events to the server and the server sends a stream of commands back.

The data types used are, so far, equally incomplete. Events don’t yet contain any useful information. Commands are just a list of actions with time offsets and the only action available right now is “say”.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
// event from robot (the body) containing current state
message BodyEvent {
    int32 id = 1;
}

message Say {
    string text = 1;
}

message Action {
    // delay in milliseconds before triggering the action
    // could use the Duration type here, but don't think we need nanosecond precision and the second/nanosecond split complicates things
    int32 delay = 1;
    oneof action {
        Say say = 2;
    }
}

// message from brain to body, instructing the body do take one or more actions
message BodyCommand {
    int32 id = 1;
    repeated Action actions = 2;
}

The C++ client

As part of the build we’ll generate a C++ client stub from our service definition. We can use this to make requests of the server.

Here’s how we create an instance of our client stub:

1
2
3
std::shared_ptr <Channel> channel(grpc::CreateChannel("localhost:50051", 
    grpc::InsecureChannelCredentials()))
std::unique_ptr <TheSocialRobot::Stub> stub_(TheSocialRobot::NewStub(channel))

We’re currently hard-coding the host and port.

When we invoke the EventStream method on the client stub, we get back an object we can use to send and receive streamed messages.

1
2
3
4
ClientContext context;

std::shared_ptr <ClientReaderWriter<BodyEvent, BodyCommand>> stream(
    stub_->EventStream(&context));

Sending a stream of events to the server is straightforward:

1
2
3
4
5
6
7
8
9
10
std::thread writer([stream]() {
    std::vector <BodyEvent> events{
        MakeBodyEvent(42)
    };
    for (const BodyEvent &event: events) {
        std::cout << "Sending event " << event.id() << std::endl;
        stream->Write(event);
    }
    stream->WritesDone();
});

Reading the command stream from the server is easy too:

1
2
3
4
5
6
7
BodyCommand command;
while (stream->Read(&command)) {
    std::cout << "Got message " << command.id()  << std::endl;
    for (const Action& action: command.actions()) {
        // do something with the action               
    }
}

In order to make use of the actions we need to determine what they are. Currently we just use a switch statement

1
2
3
4
5
6
7
8
9
10
11
switch (action.action_case()) {
    case Action::kSay:
    {
        const Say& say = action.say();
        std::cout << "Say: '" << say.text() << "' with delay " << action.delay() << std::endl;
        break;
    }
    default:
        std::cout << "invalid action" << std::endl;
        break;
}

Building it

The annoying thing about gRPC for C++ is that we need the gRPC source as part of our build. C++ also does not have a great dependency management story which means that in the past I’ve had to manually install dependencies of the projects I’m building.

However, now there is conan the C/C++ package manager developed by JFrog and gRPC is one of the packages currently available. One of the nice things about conan is that packages contain build recipes so that if a binary built with the options you need is not available conan can transparently download the source and build it. Without this ability there would be no chance of compiling code for the robot. I have not yet tried cross-compilation with conan so I don’t know how well that works. conan also integrates with CMake which is required for building with the NAOqi C++ SDK.

So I was all set to use conan…

However, although I did manage to build a test C++ project with gRPC and conan I wasn’t able to get conan to work with the qibuild CMake tooling required to use the cross-compilation toolchain. This may have been a stupid mistake on my part, but I lost patience once I found there was a CMake native approach I could use.

Enter CMake FetchContent.

Since CMake 3.11 it’s been possible to get CMake to fetch content at build configure time and this can be used to download dependencies. Thankfully, this is the approach used by the gRPC projects own C++ examples and so I was able to adapt the this code.

First we need to tell CMake to fetch the correct gRPC source. We also need to get a reference to the protobuf library.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
  include(FetchContent)
  FetchContent_Declare(
    gRPC
    GIT_REPOSITORY https://github.com/grpc/grpc
    GIT_TAG        v1.43.0
  )
  set(FETCHCONTENT_QUIET OFF)
  FetchContent_MakeAvailable(gRPC)

  # Since FetchContent uses add_subdirectory under the hood, we can use
  # the grpc targets directly from this build.
  set(_PROTOBUF_LIBPROTOBUF libprotobuf)
  set(_REFLECTION grpc++_reflection)
  set(_PROTOBUF_PROTOC $<TARGET_FILE:protoc>)
  set(_GRPC_GRPCPP grpc++)
  if(CMAKE_CROSSCOMPILING)
    find_program(_GRPC_CPP_PLUGIN_EXECUTABLE grpc_cpp_plugin)
  else()
    set(_GRPC_CPP_PLUGIN_EXECUTABLE $<TARGET_FILE:grpc_cpp_plugin>)
  endif()

We also need to setup the generation of the C++ stub code from our protobuf definition.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
# Need to have BrainCore checked out in same directory as NaoBody
get_filename_component(tsr_proto "../../BrainCore/thesocialrobot/thesocialrobot.proto" ABSOLUTE)
get_filename_component(tsr_proto_path "${tsr_proto}" PATH)

# Generated sources
set(tsr_proto_srcs "${CMAKE_CURRENT_BINARY_DIR}/thesocialrobot.pb.cc")
set(tsr_proto_hdrs "${CMAKE_CURRENT_BINARY_DIR}/thesocialrobot.pb.h")
set(tsr_grpc_srcs "${CMAKE_CURRENT_BINARY_DIR}/thesocialrobot.grpc.pb.cc")
set(tsr_grpc_hdrs "${CMAKE_CURRENT_BINARY_DIR}/thesocialrobot.grpc.pb.h")
add_custom_command(
      OUTPUT "${tsr_proto_srcs}" "${tsr_proto_hdrs}" "${tsr_grpc_srcs}" "${tsr_grpc_hdrs}"
      COMMAND ${_PROTOBUF_PROTOC}
      ARGS --grpc_out "${CMAKE_CURRENT_BINARY_DIR}"
        --cpp_out "${CMAKE_CURRENT_BINARY_DIR}"
        -I "${tsr_proto_path}"
        --plugin=protoc-gen-grpc="${_GRPC_CPP_PLUGIN_EXECUTABLE}"
        "${tsr_proto}"
      DEPENDS "${tsr_proto}")

# Include generated *.pb.h files
include_directories("${CMAKE_CURRENT_BINARY_DIR}")

Finally, we build the generated code as a library and use qibuild to create our executable.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
# tsr_grpc_proto
add_library(tsr_grpc_proto
  ${tsr_grpc_srcs}
  ${tsr_grpc_hdrs}
  ${tsr_proto_srcs}
  ${tsr_proto_hdrs})
target_link_libraries(tsr_grpc_proto
  ${_REFLECTION}
  ${_GRPC_GRPCPP}
  ${_PROTOBUF_LIBPROTOBUF})

# Create a executable named body
# with the source file: main.cpp
qi_create_bin(body "main.cpp")
target_link_libraries(body
    tsr_grpc_proto
    ${_REFLECTION}
    ${_GRPC_GRPCPP}
    ${_PROTOBUF_LIBPROTOBUF})

Automating the build

Since we already had a GitHub Action to build the previous “hello world” C++ app the only thing we needed to change was to move the NaoBody checkout to a sub-directory, add another checkout step to get the protobuf file from the “brain” repository and ensure that our build command runs in the correct directory.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
jobs:
  build:
    runs-on: ubuntu-latest
    container: 
      # generally using the latest tag is not a great idea, but in this case it's under our control
      image: thesocialrobot/naobuild:latest
    steps:
    # existing checkout step with path added
    - name: Checkout body
      uses: actions/checkout@v3
      with:
          path: 'NaoBody'
    
    # new checkout step
    - name: Checkout brain code
      uses: actions/checkout@v3
      with:
          repository: TheSocialRobot/BrainCore
          path: 'BrainCore'

    - name: Build desktop
      shell: bash
      run: ./make-desktop.sh
      working-directory: ./NaoBody # This line is the only change to the "Build desktop" step

Are we there yet?

Sadly, we are not done yet. Although everything above works fine and I can run the C++ client with the go server on my desktop I haven’t yet got cross-compilation working so I can’t deploy to the NAO robot. The issue seems to be that the cross-compilation toolchain is configured to use an old version of the C standard that does not support features used in some of the gRPC dependencies.

1
2
3
4
5
6
7
[  1%] Building C object _deps/grpc-build/CMakeFiles/address_sorting.dir/third_party/address_sorting/address_sorting.c.o
/app/build/NaoBody/body/build-cross-atom/_deps/grpc-src/third_party/address_sorting/address_sorting.c: In function 'address_sorting_rfc_6724_sort':
/app/build/NaoBody/body/build-cross-atom/_deps/grpc-src/third_party/address_sorting/address_sorting.c:351:3: error: 'for' loop initial declarations are only allowed in C99 mode
/app/build/NaoBody/body/build-cross-atom/_deps/grpc-src/third_party/address_sorting/address_sorting.c:351:3: note: use option -std=c99 or -std=gnu99 to compile your code
make[2]: *** [_deps/grpc-build/CMakeFiles/address_sorting.dir/build.make:76: _deps/grpc-build/CMakeFiles/address_sorting.dir/third_party/address_sorting/address_sorting.c.o] Error 1
make[1]: *** [CMakeFiles/Makefile2:1068: _deps/grpc-build/CMakeFiles/address_sorting.dir/all] Error 2
make: *** [Makefile:136: all] Error 2

Obviously I need to get this to work or abandon gRPC :-(

The story continues…

All rights reserved by the author.

How to use bi-directional gRPC in go

State of the Chatbot - ChatGPT

Comments powered by Disqus.