PB反射技巧整理

Protobuff是一个与开发语言、平台无关序列化的工具,广泛应用于需要跨进程传输数据的场景。除此之外,PB还提供了强大的反射能力。可以利用反射的技巧,可以大量降低重复的代码。对于C++来说,语言层面是默认不支持反射的,PB的反射可以很好的补充cpp不支持反射的问题。

什么是反射?
计算机程序在运行时可以访问、检测和修改它本身状态或行为

开局一张图:

反射可以干什么:

// 比如:根据对象的名称 去动态的创建出一个对象
int pb_reflect()
{
    // "PersonInfo" 创建一个PersonInfo的对象
    const google::protobuf::Descriptor * descriptor = google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName("PersonInfo");
    const google::protobuf::Message * prototype = google::protobuf::MessageFactory::generated_factory()->GetPrototype(descriptor);
    google::protobuf::Message* instance = prototype->New();

    // 反射相关接口
    // 给PersonInfo对象里面的某个字段设置值
    const google::protobuf::Reflection * reflecter = instance->GetReflection();
    const google::protobuf::FieldDescriptor * field = descriptor->FindFieldByName("name");
    reflecter->SetString(instance, field, "gagagaga") ;

    // 打印下.
    std::cout<GetString(*instance , field)<< std::endl ;
    cout << "string_to_pb >>>> " << instance->DebugString() << endl;
    return 0 ;    
}

在pb的设计中,任何一个pb对象都是message类派生出来的对象。那么,对于任何一个pb对象,都可以拿到它的Descriptor 和 Reflection。也就是说,对于任何一个pb对象,都可以拿到这两个类,从而找到pb对象的描述信息和反射信息。

  • Descriptor 的使用
    Descriptor 中包含的message对象里面定义的字段,通过遍历可以依次拿到里面的field,包括field的对象类型,名称,是否是数组等各种信息。
int print_pb_message(const google::protobuf::Message & message)
{
    const google::protobuf::Descriptor* pDescriptor = message.GetDescriptor();
    int count = pDescriptor->field_count();
    cout << "field count : " << count << "  message : " 
         << pDescriptor->name() << " file desc" << pDescriptor->file()<< endl;

    for (int i = 0; i < count; i++)
    {
        const google::protobuf::FieldDescriptor * pFieldDesc = pDescriptor->field(i);
        cout << "field index : " << i << ", field name :" << pFieldDesc->name() 
             << ", field type : " << pFieldDesc->cpp_type() << ", is repeated : " 
             << pFieldDesc->is_repeated() << ", number : " << pFieldDesc->number()  << endl;

        // option
        pFieldDesc->options();
    }
    cout << message.DebugString() << endl;
    return 0;
}
  • Reflection 的使用
    refection提供了动态获得message对象的值和修改它的能力。
int fill_pb_message(google::protobuf::Message & message)
{
    const google::protobuf::Reflection* pReflection = message.GetReflection();

    const google::protobuf::Descriptor* pDescriptor = message.GetDescriptor();
    int count = pDescriptor->field_count();

    for (int i = 0; i < count; i++)
    {
        const google::protobuf::FieldDescriptor * field = pDescriptor->field(i);
        bool has_field = pReflection->HasField(message, field);
        if (has_field)
        {
            if (field->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_UINT32)
            {
                cout << pReflection->GetUInt32(message, field) << endl;

                // 利用反射填充对象
                int field_value = 666;
                pReflection->SetUInt32(&message, field, field_value);
            }
        }
    }
    return 0;
}
/* proto 定义
message PersonInfo
{
    uint32 id     = 1;
    uint32 age    = 2;
    uint32 passwd = 3;
    string name   = 4;
}
*/

int main()
{
    PersonInfo info;
    info.set_age(100);
    cout <<"debug string : " << info.DebugString() << endl;

    print_pb_message(info);
    fill_pb_message(info);
    cout <<"debug string : " << info.DebugString() << endl;
}

// 输出如下:
debug string : age: 100

field count : 4  message : PersonInfo file desc0x435c60
field index : 0, field name :id, field type : 3, is repeated : 0, number : 1
field index : 1, field name :age, field type : 3, is repeated : 0, number : 2
field index : 2, field name :passwd, field type : 3, is repeated : 0, number : 3
field index : 3, field name :name, field type : 9, is repeated : 0, number : 4
age: 100
100
debug string : age: 666

PB反射的几个妙用 :

  1. 根据字符串生成对象
  2. 利用反射Descriptor生成代码,减少冗余代码
  3. 利用自定义扩展生成相关代码

举个栗子展开一点点来整理:

  • 例子1:根据字符串生成对象
    根据输入的对象名称,比如"PersonInfo",去创建一个实际的PersonInfo对象。并且可以设置和修改PersonInfo对象中任何一个字段的值。
message PersonInfo
{
    uint32 id     = 1;
    uint32 age    = 2;
    uint32 passwd = 3;
    string name   = 4;
}
int string_to_pb()
{
    google::protobuf::Message* message = pb_create_by_name("PersonInfo");
    
    std::queue args;
    args.push("10001");
    args.push("18");
    args.push("123456");
    args.push("haha");

    int ret = pb_fill_message(args, message);
    if (ret != 0) {
        cout << "fill failed" << endl;
        return ret;
    }
    cout << "string_to_pb >>>> " << message->DebugString() << endl;
    return 0;
}

// 输出:
id: 10001
age: 18
passwd: 123456
name: "haha"
  • 例子2:利用反射Descriptor生成代码,减少冗余代码
    对于任何一个pb对象,把他装换为一个map。里面的key和value分别是字段的名称和它对应的数值。并且当pb新增字段或者修改字段的时候,这段函数不需要随着它来调整。
int PbToMap(const google::protobuf::Message &message,
            std::map &out) 
{
    #define CASE_FIELD_TYPE(cpptype, method, valuetype)                            \
    case google::protobuf::FieldDescriptor::CPPTYPE_##cpptype: {                   \
        valuetype value = reflection->Get##method(message, field);                 \
        std::ostringstream oss;                                                    \
        oss << value;                                                              \
        out[field->name()] = oss.str();                                            \
        break;                                                                     \
    }

    #define CASE_FIELD_TYPE_ENUM()                                                 \
    case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: {                        \
        int value = reflection->GetEnum(message, field)->number();                 \
        std::ostringstream oss;                                                    \
        oss << value;                                                              \
        out[field->name()] = oss.str();                                            \
        break;                                                                     \
    }

    #define CASE_FIELD_TYPE_STRING()                                               \
    case google::protobuf::FieldDescriptor::CPPTYPE_STRING: {                      \
        std::string value = reflection->GetString(message, field);                 \
        out[field->name()] = value;                                                \
        break;                                                                     \
    }

    const google::protobuf::Descriptor *descriptor = message.GetDescriptor();
    const google::protobuf::Reflection *reflection = message.GetReflection();

    for (int i = 0; i < descriptor->field_count(); i++) 
    {
        const google::protobuf::FieldDescriptor *field = descriptor->field(i);
        bool has_field = reflection->HasField(message, field);

        if (has_field) 
        {
            if (field->is_repeated()) 
            {
                return -1; // 不支持转换repeated字段
            }

            const std::string &field_name = field->name();
            switch (field->cpp_type()) 
            {
                CASE_FIELD_TYPE(INT32, Int32, int);
                CASE_FIELD_TYPE(UINT32, UInt32, uint32_t);
                CASE_FIELD_TYPE(FLOAT, Float, float);
                CASE_FIELD_TYPE(DOUBLE, Double, double);
                CASE_FIELD_TYPE(BOOL, Bool, bool);
                CASE_FIELD_TYPE(INT64, Int64, int64_t);
                CASE_FIELD_TYPE(UINT64, UInt64, uint64_t);
                CASE_FIELD_TYPE_ENUM();
                CASE_FIELD_TYPE_STRING();
            default:
                return -1; // 其他异常类型
            }
        }
    }
    return 0;
} 

int main()
{
    std::map pb_map;
    PbToMap(info, pb_map);
}

// pb转换为map
// 输出
(gdb) p pb_map
$4 = std::map with 1 element = {["age"] = "666"}
  • 利用自定义扩展生成相关代码
    利用pb中的扩展字段,自动生成相关的代码,实现配置化的开发。当需要check的规则调整是,不需要改动代码。只需要修改proto的定义即可。
syntax = "proto3";

import "google/protobuf/descriptor.proto";

message PersonInfo
{
    uint32 id     = 1;
    uint32 age    = 2;
    uint32 passwd = 3;
    string name   = 4;
}

message FieldRule{
     uint32 length_min = 1; // 字段最小长度
     uint32 id         = 2; // 字段映射id
}

extend google.protobuf.FieldOptions{
     FieldRule field_rule = 50000;
}

message Student{
     string name   =1 [(field_rule).length_min = 5, (field_rule).id = 1];
     string email = 2 [(field_rule).length_min = 10, (field_rule).id = 2];
}
int allCheck(const google::protobuf::Message &oMessage){
    const auto *poReflect = oMessage.GetReflection();

    vector vecFD;
    poReflect->ListFields(oMessage, &vecFD);

    for (const auto &poFiled : vecFD) {
        const auto &oFieldRule = poFiled->options().GetExtension(field_rule);
        if (poFiled->cpp_type() == google::protobuf::FieldDescriptor::CPPTYPE_STRING && !poFiled->is_repeated()) {
            // 类型是string并且选项非重复的才会校验字段长度类型
            const std::string strValue = poReflect->GetString(oMessage, poFiled);
            const std::string strName = poFiled->name();

            if (oFieldRule.length_min()) {
                // 有才进行校验,没有则不进行校验
                if (minLengthCheck(strValue, oFieldRule.length_min())) {
                    cout << "the length of " << strName << " is lower than " << oFieldRule.length_min()<

这里只整理了核心代码,全部的代码移步:PB REFLECT


  • PB的反射是什么实现的?
    实现反射需要关键的两个点,第一,需要保存一个对象名称和对象实际定义的一个映射关系;第二,要知道一个对象的内存布局,对象中每个字段的内存偏移;对于pb而言,这些信息都存在于proto文件中。

    1、提供 .proto (范指 ProtoBuf Message 语法描述的元信息)

    2、解析 .proto 构建 FileDescriptor、FieldDescriptor 等,即 .proto 对应的内存模型(对象)

    3、创建一个实例,就将其存到相应的实例池中

    4、将 Descriptor 和 instance 的映射维护到表中备查

    5、通过 Descriptor 可查到相应的 instance,又由于了解 instance中字段类型(FieldDescriptor),所以知道字段的内存偏移,那么就可以访问或修改字段的值。任何一个对象最终都对应一段内存,有内存起始(start_addr)和结束地址, 而对象的每一个属性,都位于start_addr+$offset ,所以当对象和对应属性的offset已知的时候, 属性的内存地址也就是可以获取的。


小结:

PB除了被用于序列化和反序列化之外,这里总结了PB的一些技反射巧。利用pb的反射,可以减少写冗余代码的开发。但是,pb运行时的反射性能不是很好,官方也在文档中提到了这一点,要注意使用的场景。


关于pb的其他内容:
* PB插件开发指南
* protobuff的序列化和反序列化编码实现
参考:
https://cloud.tencent.com/developer/article/1753977
https://blog.csdn.net/JMW1407/article/details/107223287

你可能感兴趣的:(PB反射技巧整理)