ProtoBuf 反射詳解
最初是起源於這樣一個問題:
給定一個pb物件,如何自動遍歷該物件的所有欄位?
即是否有一個通用的方法可以遍歷任意pb物件的所有欄位,而不用關心具體物件型別。
使用場景上有很多:
比如json格式字串的相互轉換,或者bigtable里根據pb物件的欄位自動寫列名和對應的value。
例如定義了pb messge型別Person如下:
123 | Person |
能否將該物件自動轉化為json字串{"name":"yingshin","age":21}
,或者自動的寫hbase裡的多列:
key | column-name | column-value |
---|---|---|
uid | name | yingshin |
uid | age | 21 |
如果設定了新的欄位,比如person.set_email("[email protected]")
,則自動新增新的一列:
key | column-name | column-value |
---|---|---|
uid | [email protected] |
答案就是 pb的反射功能。
我們的目標是提供這樣兩個介面:
12345 | //從給定的message物件序列化為固定格式的字串voidserialize_message(constgoogle::protobuf::Message&message,std::string*serialized_string);//從給定的字串按照固定格式還原為原message物件voidparse_message(conststd::string&serialized_string,google::protobuf::Message*message); |
接下來逐步介紹下如何實現。
1. 反射相關介面
要介紹pb的反射功能,先看一個相關的UML示例圖:
各個類以及介面說明:
1.1 Message
Person是自定義的pb型別,繼承自Message. MessageLite作為Message基類,更加輕量級一些。
通過Message的兩個介面GetDescriptor/GetReflection
,可以獲取該型別對應的Descriptor/Reflection。
1.2 Descriptor
Descriptor是對message型別定義的描述,包括message的名字、所有欄位的描述、原始的proto檔案內容等。
本文中我們主要關注跟欄位描述相關的介面,例如:
- 獲取所有欄位的個數:
int field_count() const
- 獲取單個欄位描述型別
FieldDescriptor
的介面有很多個,例如
123 | constFieldDescriptor*field(intindex)const;//根據定義順序索引獲取constFieldDescriptor*FindFieldByNumber(intnumber)const;//根據tag值獲取constFieldDescriptor*FindFieldByName(conststring&name)const;//根據field name獲取 |
1.3 FieldDescriptor
FieldDescriptor描述message中的單個欄位,例如欄位名,欄位屬性(optional/required/repeated)等。
對於proto定義裡的每種型別,都有一種對應的C++型別,例如:
123 | enumCppType{CPPTYPE_INT32=1,//TYPE_INT32, TYPE_SINT32, TYPE_SFIXED32} |
獲取型別的label屬性:
1234567 | enumLabel{LABEL_OPTIONAL=1,//optionalLABEL_REQUIRED=2,//requiredLABEL_REPEATED=3,//repeatedMAX_LABEL=3,//Constant useful for defining lookup tables indexed by Label.} |
獲取欄位的名稱:const string& name() const;
。
1.4 Reflection
Reflection主要提供了動態讀寫pb欄位的介面,對pb物件的自動讀寫主要通過該類完成。
對每種型別,Reflection都提供了一個單獨的介面用於讀寫欄位對應的值。
例如對於讀操作:
1234 | virtual int32 GetInt32(constMessage&message,constFieldDescriptor*field)const=0;virtual int64 GetInt64(constMessage&message,constFieldDescriptor*field)const=0; |
特殊的,對於列舉和巢狀的message:
123456 | virtual constEnumValueDescriptor*GetEnum(constMessage&message,constFieldDescriptor*field)const=0;// See MutableMessage() for the meaning of the "factory" parameter.virtual constMessage&GetMessage(constMessage&message,constFieldDescriptor*field,MessageFactory*factory=NULL)const=0; |
對於寫操作也是類似的介面,例如SetInt32/SetInt64/SetEnum
等。
2. 反射示例
示例主要是接收任意型別的message物件,遍歷解析其中的每個欄位、以及對應的值,按照自定義的格式儲存到一個string中。同時重新反序列化該string,讀取欄位以及value,填充到message物件中。例如:
其中Person是自定義的protobuf message型別,用於設定一些欄位驗證我們的程式。
單純的序列化/反序列化功能可以通過pb自帶的SerializeToString/ParseFromString介面完成。這裡主要是為了同時展示自動從pb物件裡提取field/value,自動根據field/value來還原pb物件這個功能。
123456789101112131415161718192021 | intmain(){std::stringserialized_string;{tutorial::Person person;person.set_name("yingshin");person.set_id(123456789);person.set_email("[email protected]");::tutorial::Person_PhoneNumber*phone=person.mutable_phone();phone->set_type(tutorial::Person::WORK);phone->set_number("13266666666");serialize_message(person,&serialized_string);}{tutorial::Person person;parse_message(serialized_string,&person);}return0;} |
其中Person定義是對example裡的addressbook.proto做了少許修改(修改的原因是本文沒有涉及pb裡陣列的處理)
1234567891011121314151617181920 | packagetutorial;messagePerson{required stringname=1;required int32 id=2;// Unique ID number for this person.optional stringemail=3;enumPhoneType{MOBILE=0;HOME=1;WORK=2;}messagePhoneNumber{required stringnumber=1;optional PhoneType type=2[default=HOME];}optional PhoneNumber phone=4;} |
3. 反射例項實現
3.1 serialize_message
serialize_message遍歷提取message中各個欄位以及對應的值,序列化到string中。主要思路就是通過Descriptor得到每個欄位的描述符:欄位名、欄位的cpp型別。通過Reflection的GetX介面獲取對應的value。
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273 | voidserialize_message(constgoogle::protobuf::Message&message,std::string*serialized_string){constgoogle::protobuf::Descriptor*descriptor=message.GetDescriptor();constgoogle::protobuf::Reflection*reflection=message.GetReflection();for(inti=0;ifield_count();++i){constgoogle::protobuf::FieldDescriptor*field=descriptor->field(i);boolhas_field=reflection->HasField(message,field);if(has_field){//arrays not supportedassert(!field->is_repeated());switch(field->cpp_type()){#define CASE_FIELD_TYPE(cpptype, method, valuetype)casegoogle::protobuf::FieldDescriptor::CPPTYPE_##cpptype:{valuetype value=reflection->Get##method(message, field);intwsize=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_TYPEcasegoogle::protobuf::FieldDescriptor |