草庐IT

PB反射技巧整理

DayDayUpppppp 2023-03-28 原文

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<<reflecter->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<std::string> 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<std::string, std::string> &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<std::string,std::string> 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<const FieldDescriptor *> 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()<<endl;
                } else {
                    cout << "check min lenth pass"<<endl;
                }
            }
        }
    }
    return 0;
}

int main() {
    Student oStudent1;
    oStudent1.set_name("xiao");

    Student oStudent2;
    oStudent2.set_name("xiaowei");

    allCheck(oStudent1);
    allCheck(oStudent2);

    return 0;
}

// 输出
the length of name is lower than 5
check min lenth pass

这里只整理了核心代码,全部的代码移步: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反射技巧整理的更多相关文章

  1. 动漫制作技巧如何制作动漫视频 - 2

    动漫制作技巧是很多新人想了解的问题,今天小编就来解答与大家分享一下动漫制作流程,为了帮助有兴趣的同学理解,大多数人会选择动漫培训机构,那么今天小编就带大家来看看动漫制作要掌握哪些技巧?一、动漫作品首先完成草图设计和原型制作。设计草图要有目的、有对象、有步骤、要形象、要简单、符合实际。设计图要一致性,以保证制作的顺利进行。二、原型制作是根据设计图纸和制作材料,可以是手绘也可以是3d软件创建。在此步骤中,要注意的问题是色彩和平面布局。三、动漫制作制作完成后,加工成型。完成不同的表现形式后,就要对设计稿进行加工处理,使加工的难易度降低,并得到一些基本准确的概念,以便于后续的大样、准确的尺寸制定。四、

  2. Unity Shader 学习笔记(5)Shader变体、Shader属性定义技巧、自定义材质面板 - 2

    写在之前Shader变体、Shader属性定义技巧、自定义材质面板,这三个知识点任何一个单拿出来都是一套知识体系,不能一概而论,本文章目的在于将学习和实际工作中遇见的问题进行总结,类似于网络笔记之用,方便后续回顾查看,如有以偏概全、不祥不尽之处,还望海涵。1、Shader变体先看一段代码......Properties{ [KeywordEnum(on,off)]USL_USE_COL("IsUseColorMixTex?",int)=0 [Toggle(IS_RED_ON)]_IsRed("IsRed?",int)=0}......//中间省略,后续会有完整代码 #pragmamulti_c

  3. H2数据库配置及相关使用方式一站式介绍(极为详细并整理官方文档) - 2

    目录H2数据库入门以及实际开发时的使用1.H2数据库的初识1.1H2数据库介绍1.2为什么要使用嵌入式数据库?1.3嵌入式数据库对比1.3.1性能对比1.4技术选型思考2.H2数据库实战2.1H2数据库下载搭建以及部署2.1.1H2数据库的下载2.1.2数据库启动2.1.2.1windows系统可以在bin目录下执行h2.bat2.1.2.2同理可以通过cmd直接使用命令进行启动:2.1.2.3启动后控制台页面:2.1.3spring整合H2数据库2.1.3.1引入依赖文件2.1.4数据库通过file模式实际保存数据的位置2.2H2数据库操作2.2.1Mysql兼容模式2.2.2Mysql模式

  4. ruby-on-rails - 使用模型属性调用的 Brakeman 不安全反射方法常量化 - 2

    在我的Rails应用程序中,我收到来自brakeman的以下安全警告。使用模型属性调用的不安全反射方法常量化。这是我的代码正在执行的操作。chart_type=Chart.where(id:chart_id,).pluck(:type).firstbeginChartPresenter.new(chart_type.camelize.constantize.find(chart_id))rescueraise"Unabletofindthechartpresenter"end根据我的研究,我还没有找到任何具体的解决方案。我听说你可以创建一个白名单,但我不确定brakeman在寻找什么。

  5. ruby - ruby 的反射? - 2

    我很好奇这是如何工作的。例如,如果我创建一个基于工厂模式的类,您可以在其中“注册”类供以后使用,然后执行类似FactoryClass.register('YourClassName',[param,param,...]);FactoryClass.create('your_class_name').call_method_from_this_object其中'class_name'是映射到值的散列中的键:ClassName有没有类似phpreflection的东西,我可以在哪里创建基于字符串名称的类的实例并传入参数?(在php中,参数将是它们的数组,php然后知道如何处理)所以如果我们

  6. ruby-on-rails - 将 vim 与 ruby​​/ruby on rails 结合使用的提示和技巧 - 2

    我是那些没有在他的任何Ruby/RubyonRails工作中使用TextMate的开发人员之一。我在这个领域的特别忠诚在于vim。您最喜欢将vim与Ruby和/或RubyonRails结合使用以尽可能提高工作效率的提示/技巧是什么? 最佳答案 最重要获取rails.vim的副本它在数百万级别上很棒。Readthedoc.提示太多了,:Rviewcustomer,:RSmodelfoo,:Rinvert,gf,:Rextract,:Rake等等。您可能需要NERDTree以及轻松导航(您可以使用:Rtree访问)第二重要在推特上关注t

  7. 嵌入式单元测试工具Tessy的一些测试技巧 - 2

    最近做了一个平台项目,需要进行动态代码测试,入门了嵌入式单元测试工具Tessy,总结了一些简单的测试技巧。当前网上的教程普遍只写内容概要,真正入手还得自己认真摸索一番。为此,特意总结了一些Tessy测试技巧以供有缘人参考。提几个Tessy工具使用的问题。1.如何导入工程的头文件?2.如何解决文件内存在汇编语言代码分析时候报错的问题?3.如何规避本文件创建却不使用的函数,宏和变量,在执行executetest出现的undefinedreferencetoxx的问题?4.如何创建测试用例testcase?5.为什么testcase的结果输出与期望不一致?6.创建testcase的方法有几种?7.C

  8. ruby - Ruby 中的反射。通过给定的类名实例化一个对象 - 2

    我是从PHP开始接触ruby​​的。我怎么能用ruby​​做下一件事?$className='ArrayObject';$arrayObject=new$className(); 最佳答案 你可以这样做:arrayObject=Object::const_get('Array').new 关于ruby-Ruby中的反射。通过给定的类名实例化一个对象,我们在StackOverflow上找到一个类似的问题: https://stackoverflow.com/qu

  9. ruby - 如何使用反射获取参数名称 - 2

    我想在Ruby中做一些相当繁重的反射。我想创建一个函数,该函数返回调用堆栈更高层的各种调用函数的参数名称(只要高一点就足够了,但为什么要停在那里?)。我可以使用Kernel.caller,转到文件并解析参数列表,但这会很丑陋且不可靠。我想要的功能将按以下方式工作:moduleAdefmethod1(tuti,fruity)fooenddefmethod2(bim,bam,boom)fooenddeffooprintcaller_args[1].join(",")#the"1"meanonestepupthecallstackendendA.method1#prints"tuti,fru

  10. ruby-on-rails - 如何在数据库中反射(reflect) Ruby on Rails 中新的 belongs_to 和 has_many 关系 - 2

    我是Rails的新手(通常是Python专家),只是为了好玩而尝试构建一个简单的任务管理器应用程序。我正在使用Devise进行身份验证,并且有一个我试图与用户关联的任务对象。我已将以下内容添加到任务模型中:classTask并且我在我的Devise用户模型中添加了以下内容:classUser>end每当我添加此信息时,我都会运行:rakedb:migrate。然后它给了我一个错误,当我试图用它做任何事情时,user_id的数据库字段不存在。我确信这是我所缺少的相当简单的东西。感谢您的帮助。 最佳答案 向模型添加belongs_to(

随机推荐