Protobuf反射详解

所谓反射机制,就是能够在运行时知道任意类的所有属性和方法,能够调用任意对象的任意方法和属性。这种动态获取的信息以及动态调用对象方向的功能称为反射机制。

不像Jave等语言,C++本身没有反射机制,在使用C++版本Protobuf时,protobuf通过proto文件产生响应的message和service,protobuf可以通过proto文件提供反射机制,程序在运行时可以通过proto获取任意message和任意service的属性和方法,也可以在运行时调用message的属性和方法。

1.获取message和service的属性和方法

protobuf通过Descriptor获取任意message或service的属性和方法,Descriptor主要包括了一下几种类型:

FileDescriptor 获取Proto文件中的Descriptor和ServiceDescriptor
Descriptor 获取类message属性和方法,包括FieldDescriptor和EnumDescriptor
FieldDescriptor 获取message中各个字段的类型、标签、名称等
EnumDescriptor 获取Enum中的各个字段名称、值等
ServiceDescriptor 获取service中的MethodDescriptor
MethodDescriptor 获取各个RPC中的request、response、名称等

也就是说,如果能够获取到proto文件的FileDescriptor,就能获取proto文件中的所有的内容。那么如何获取proto文件的FileDescriptor呢?protobuf提供两种方法。

1.1 使用protoc将proto文件生成.h和.cc文件

这种法法直接根据生成的类来获取响应的FileDescriptor,比如现在有test.proto文件,那么可以通过DescriptorPool::generated_pool()获取到其FileDescriptor

const FileDescriptor* fileDescriptor = DescriptorPool::generated_pool()->FindFileByName(file);

并且对于任意的message和service都可以根据其名称,通过DescriptorPool对应的Descriptor和ServiceDescriptor。

1.2 只使用proto文件,不使用protoc进行编译

这种情况需要手动解析proto文件,再获取FileDescriptor。protobuf提供了响应的解析器compiler,通过compoiler可以方便的获取proto文件的FileDescriptor

const FileDescriptor* GetFileDescriptorFromProtoFile(const std::string &protoRoot, const std::string &protoFile){
    compiler::DeskSourceTree sourceTree;
    sourceTree.MapPath("", protoRoot);
    FileErrorCollector errorCollector;
    compiler::Importer importer(&sourceTree, &errorCollector);
    return importer.Import(protoFile);
}

2. 调用message的属性和方法

首先获取message的对象:

const Message* prototype = MessageFactory::generated_factory()->GetPrototype(descriptor);
if(!prototype){
    continue;
}
Message *message = prototype->New();

有了message对象,因为所有的message都是Message* 类型的,但不同的message对象的属相和方法是不一样的,所以并不能直接调用其对象和方法。

protobuf是通过Reflection来调用message的属性和方法的。message中的方法只有对各个属性的get和set,而调用message的属性其实也就是调用属性的get。调用message的某一个属性的get和set,就需要该属性的Descriptor,通过Reflection将相应的值写入到message对应的属性:

const FieldDescriptor * field_descriptor = message_descriptor->field(i);
if(field_descriptor->type() == FieldDescriptor::TYPE_INT32){
    reflection->SetInt32(message, field_descriptor, 0);
}

3.Protobuf反射使用案例

这部分主要是重复上两节的内容,进行完整示例。以下是使用的test.proto文件

package T;
message Test
{
    optional int32 id = 1;
}

3.1通过类型名字创建出类型对象

预编译好proto模式

//! 利用类型名字构造对象.
/*!
 * @Param type_name 类型名字,比如 "Test.TestMessage".
 * @Return 对象指针,new 出来的,使用者负责释放.
 */
#include <google/protobuf/descriptor.h>
#include <google/protobuf/message.h>
#include "cpp/test.pb.h" // 这是protoc给你生成的文件

int main()
{
    // 先获得类型的Descriptor .
    auto descriptor = google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName("T.Test");
    if (nullptr == descriptor)
    {
        return 0 ;
    }
    // 利用Descriptor拿到类型注册的instance. 这个是不可修改的.
    auto prototype = google::protobuf::MessageFactory::generated_factory()->GetPrototype(descriptor);
    if ( nullptr == descriptor)
    {
        return 0 ;
    }
    // 构造一个可用的消息.
    auto message = prototype->New();
    // 只有当我们预先编译了test消息并且正确链接才能这么干.
    auto test = dynamic_cast<T::Test*>(message);
    // 直接调用message的具体接口
    // 其实这些接口是语法糖接口.所以并没有对应的反射机制来对应调用.
    // 反射机制实现了的Set/Get XXX系列接口,是属于Reflection的接口,接收Message作为参数.
    test->set_id(1);
    std::cout<<test->Utf8DebugString()<<std::endl;
    delete message ;
    return 0 ;
}

直接解析proto文件模式

#include <iostream>
#include <google/protobuf/compiler/importer.h>
#include <google/protobuf/dynamic_message.h>
int main()
{
    // 准备配置好文件系统
    google::protobuf::compiler::DiskSourceTree sourceTree;
    // 将当前路径映射为项目根目录 , project_root 仅仅是个名字,你可以你想要的合法名字.
    sourceTree.MapPath("project_root","./");
    // 配置动态编译器.
    google::protobuf::compiler::Importer importer(&sourceTree, NULL);
    // 动态编译proto源文件。 源文件在./source/proto/test.proto .
    importer.Import("project_root/source_proto/test.proto");
    // 现在可以从编译器中提取类型的描述信息.
    auto descriptor1 = importer.pool()->FindMessageTypeByName("T.Test");
    // 创建一个动态的消息工厂.
    google::protobuf::DynamicMessageFactory factory;
    // 从消息工厂中创建出一个类型原型.
    auto proto1 = factory.GetPrototype(descriptor1);
    // 构造一个可用的消息.
    auto message1= proto1->New();
    // 下面是通过反射接口给字段赋值.
    auto reflection1 = message1->GetReflection();
    auto filed1 = descriptor1->FindFieldByName("id");
    reflection1->SetInt32(message1,filed1,1);
    // 打印看看
    std::cout << message1->DebugString();
    // 删除消息.
    delete message1 ;
    return 0 ;
}

3.2 通过对象和对象的属性获取名字,修改对应属性

#include "cpp/test.pb.h"
#include <iostream>

int main()
{
    // 拿到一个对象,不在乎怎么拿到,可以是通过反射拿到。
    // 这里简单直接的创建一个.
    T::Test p_test ;
    // 拿到对象的描述包.
    auto descriptor = p_test.GetDescriptor() ;
    // 拿到对象的反射配置.
    auto reflecter = p_test.GetReflection() ;
    // 拿到属性的描述包.
    auto field = descriptor->FindFieldByName("id");
    // 设置属性的值.
    reflecter->SetInt32(&p_test , field , 5 ) ;
    // 获取属性的值.
    std::cout<<reflecter->GetInt32(p_test , field)<< std::endl ;
    return 0 ;
}

4. 处理Protobuf反射,通过协议字串动态生成协议

下面两部分是针对protobuf通讯协议的内容。下面这两个函数可以动态生成pb的message,其中ProtoMsg是pb package的名字

    inline google::protobuf::Message* CreateMessage(const std::string& msg)
    {
        google::protobuf::Message* message = NULL;
        const google::protobuf::Descriptor* descriptor = google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName("ProtoMsg." + msg);
        if (descriptor)
        {
            const google::protobuf::Message* prototype = google::protobuf::MessageFactory::generated_factory()->GetPrototype(descriptor);
            if (prototype)
            {
                message = prototype->New();
            }
        }
 
        return message;
    }
 
    inline void ReleaseMessage(google::protobuf::Message* pMsg)
    {
        if (NULL != pMsg)
        {
            pMsg->Clear();
            delete pMsg;
        }
    }

5. 通过反射将配置中的值设置进pb字段

可以通过如下方法,自动创建message并对field赋值:

    // mstrCurMsg 当前正在执行的协议
    google::protobuf::Message* pMsg = Test::CreateMessage(mstrCurMsg);
    const google::protobuf::Descriptor* pDescriptor = google::protobuf::DescriptorPool::generated_pool()->FindMessageTypeByName("ProtoMsg." + mstrCurMsg);
    assert(NULL != pDescriptor);
    
    // 这一个可以获取到反射类,然后可以将配置中值赋值进去
    const google::protobuf::Reflection* pReflection = pMsg->GetReflection();
    assert(NULL != pReflection);
 
    for (int i = 0; i < pDescriptor->field_count(); ++i)
    {
        const google::protobuf::FieldDescriptor* pFieldDescriptor = pDescriptor->field(i);
        assert(NULL != pFieldDescriptor);
 
        const std::string& strFieldName = pFieldDescriptor->name();
        
        const TestConfigModule::MsgEntry* pMsgEntry = pMsgStruct->GetMsgEntry(strFieldName);
        assert(NULL != pMsgEntry);
 
        // 读取字段类型,顺带可以做类型检查
        assert(pMsgEntry->mnType == pFieldDescriptor->type());
 
        // 设置值
        switch (pMsgEntry->mnType)
        {
        case Test::TYPE_STRING:
            pReflection->SetString(pMsg, pFieldDescriptor, pMsgEntry->mstrValue);
            break;
        // ...
        default:
            break;
        }
    }
 
    std::string strData;
    if (!pMsg->SerializeToString(&strData))
    {
        m_pLogModule->LogNormal("Test stop, cannot SerializeToString ", mstrCurMsg, __FUNCTION__, __LINE__);
        return;
    }
 
    Test::ReleaseMessage(pMsg);

6. serialize_message

serialize_message遍历提取message中各个字段以及对应的值,序列化到string中。主要思路就是通过Descriptor得到每个字段的描述符:字段名、字段的cpp类型。通过Reflection的GetX接口获取对应的value。

void serialize_message(const google::protobuf::Message& message, std::string* serialized_string) {
    const google::protobuf::Descriptor* descriptor = message.GetDescriptor();
    const google::protobuf::Reflection* reflection = message.GetReflection();

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

        if (has_field) {
            //arrays not supported
            assert(!field->is_repeated());

            switch (field->cpp_type()) {
#define CASE_FIELD_TYPE(cpptype, method, valuetype)
                case google::protobuf::FieldDescriptor::CPPTYPE_##cpptype:{
                    valuetype value = reflection->Get##method(message, field);
                    int wsize = field->name().size();
                    serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize));
                    serialized_string->append(field->name().c_str(), field->name().size());
                    wsize = sizeof(value);
                    serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize));
                    serialized_string->append(reinterpret_cast(&value), sizeof(value));
                    break;
                }

                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);
#undef CASE_FIELD_TYPE
                case google::protobuf::FieldDescriptor::CPPTYPE_ENUM: {
                    int value = reflection->GetEnum(message, field)->number();
                    int wsize = field->name().size();
                    //写入name占用字节数
                    serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize));
                    //写入name
                    serialized_string->append(field->name().c_str(), field->name().size());
                    wsize = sizeof(value);
                    //写入value占用字节数
                    serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize));
                    //写入value
                    serialized_string->append(reinterpret_cast(&value), sizeof(value));
                    break;
                }
                case google::protobuf::FieldDescriptor::CPPTYPE_STRING: {
                    std::string value = reflection->GetString(message, field);
                    int wsize = field->name().size();
                    serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize));
                    serialized_string->append(field->name().c_str(), field->name().size());
                    wsize = value.size();
                    serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize));
                    serialized_string->append(value.c_str(), value.size());
                    break;
                }
                case google::protobuf::FieldDescriptor::CPPTYPE_MESSAGE: {
                    std::string value;
                    int wsize = field->name().size();
                    serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize));
                    serialized_string->append(field->name().c_str(), field->name().size());
                    const google::protobuf::Message& submessage = reflection->GetMessage(message, field);
                    serialize_message(submessage, &value);
                    wsize = value.size();
                    serialized_string->append(reinterpret_cast(&wsize), sizeof(wsize));
                    serialized_string->append(value.c_str(), value.size());
                    break;
                }
            }
        }
    }
}

7. parse_message

parse_message通过读取field/value,还原message对象。
主要思路跟serialize_message很像,通过Descriptor得到每个字段的描述符FieldDescriptor,通过Reflection的SetX填充message。

void parse_message(const std::string& serialized_string, google::protobuf::Message* message) {
    const google::protobuf::Descriptor* descriptor = message->GetDescriptor();
    const google::protobuf::Reflection* reflection = message->GetReflection();
    std::map field_map;
 
    for (int i = 0; i field_count(); ++i) {
        const google::protobuf::FieldDescriptor* field = descriptor->field(i);
        field_map[field->name()] = field;
    }
 
    const google::protobuf::FieldDescriptor* field = NULL;
    size_t pos = 0;
    while (pos (serialized_string.substr(pos, sizeof(int)).c_str()));
        pos += sizeof(int);
        std::string name = serialized_string.substr(pos, name_size);
        pos += name_size;
 
        int value_size = *(reinterpret_cast(serialized_string.substr(pos, sizeof(int)).c_str()));
        pos += sizeof(int);
        std::string value = serialized_string.substr(pos, value_size);
        pos += value_size;
 
        std::map::iterator iter =
            field_map.find(name);
        if (iter == field_map.end()) {
            fprintf(stderr, "no field found.n");
            continue;
        } else {
            field = iter->second;
        }
 
        assert(!field->is_repeated());
        switch (field->cpp_type()) {
#define CASE_FIELD_TYPE(cpptype, method, valuetype)
            case google::protobuf::FieldDescriptor::CPPTYPE_##cpptype: {
                reflection->Set##method(
                        message,
                        field,
                        *(reinterpret_cast(value.c_str())));
                std::cout name() (value.c_str())) enum_type()->FindValueByNumber(*(reinterpret_cast(value.c_str())));
                reflection->SetEnum(message, field, enum_value_descriptor);
                std::cout name() (value.c_str())) SetString(message, field, value);
                std::cout name() MutableMessage(message, field);
                parse_message(value, submessage);
                break;
            }
            default: {
                break;
            }
        }
    }
}

只是基本介绍下,在遇到问题时候还是多求助官方文档的。

《Protobuf反射详解》有1条评论

发表评论