Search code examples
c++reflectionprotocol-buffers

How to use reflection of Protobuf to modify a Map


I'm working with Protobuf3 in my C++14 project. There have been some functions, which returns the google::protobuf::Message*s as a rpc request, what I need to do is to set their fields. So I need to use the reflection of Protobuf3.

Here is a proto file:

syntax="proto3";
package srv.user;

option cc_generic_services = true;
message BatchGetUserInfosRequest {
    uint64 my_uid = 1;
    repeated uint64 peer_uids = 2;
    map<string, string> infos = 3;
}

message BatchGetUserInfosResponse {
    uint64 my_uid = 1;
    string info = 2;
}

Service UserSrv {
    rpc BatchGetUserInfos(BatchGetUserInfosRequest) returns (BatchGetUserInfosResponse);
};

Now I called a function, which returns a google::protobuf::Message*, pointing an object BatchGetUserInfosRequest and I try to set its fields.

// msg is a Message*, pointing to an object of BatchGetUserInfosRequest
auto descriptor = msg->GetDescriptor();
auto reflection = msg->GetReflection();
auto field = descriptor->FindFieldByName("my_uid");
reflection->SetUInt64(msg, field, 1234);
auto field2 = descriptor->FindFieldByName("peer_uids");
reflection->GetMutableRepeatedFieldRef<uint64_t>(msg, field2).CopyFrom(peerUids); // peerUids is a std::vector<uint64_t>

As you see, I can set my_uid and peer_uids as above, but for the field infos, which is a google::protobuf::Map, I don't know how to set it with the reflection mechanism.


Solution

  • If you dig deep into the source code, you would find out the map in proto3 is implemented on the RepeatedField:

      // Whether the message is an automatically generated map entry type for the
      // maps field.
      //
      // For maps fields:
      //     map<KeyType, ValueType> map_field = 1;
      // The parsed descriptor looks like:
      //     message MapFieldEntry {
      //         option map_entry = true;
      //         optional KeyType key = 1;
      //         optional ValueType value = 2;
      //     }
      //     repeated MapFieldEntry map_field = 1;
      //
      // Implementations may choose not to generate the map_entry=true message, but
      // use a native map in the target language to hold the keys and values.
      // The reflection APIs in such implementations still need to work as
      // if the field is a repeated message field.
      //
      // NOTE: Do not set the option in .proto files. Always use the maps syntax
      // instead. The option should only be implicitly set by the proto compiler
      // parser.
      optional bool map_entry = 7;
    

    Inspired by the test code from protobuf, this works for me:

      BatchGetUserInfosRequest message;
      auto *descriptor = message.GetDescriptor();
      auto *reflection = message.GetReflection();
    
      const google::protobuf::FieldDescriptor *fd_map_string_string =
          descriptor->FindFieldByName("infos");
      const google::protobuf::FieldDescriptor *fd_map_string_string_key =
          fd_map_string_string->message_type()->map_key();
      const google::protobuf::FieldDescriptor *fd_map_string_string_value =
          fd_map_string_string->message_type()->map_value();
    
      const google::protobuf::MutableRepeatedFieldRef<google::protobuf::Message>
          mmf_string_string =
              reflection->GetMutableRepeatedFieldRef<google::protobuf::Message>(
                  &message, fd_map_string_string);
      std::unique_ptr<google::protobuf::Message> entry_string_string(
          google::protobuf::MessageFactory::generated_factory()
              ->GetPrototype(fd_map_string_string->message_type())
              ->New(message.GetArena()));
      entry_string_string->GetReflection()->SetString(
          entry_string_string.get(), fd_map_string_string->message_type()->field(0),
          "1234");
      entry_string_string->GetReflection()->SetString(
          entry_string_string.get(), fd_map_string_string->message_type()->field(1),
          std::to_string(10));
      mmf_string_string.Add(*entry_string_string);
    
    
      std::cout << "1234: " << message.infos().at("1234") << '\n';
    

    The output:

    1234: 10