一、Message消息的可視化展示
將消息轉換為二進制結構,必然提高了結構的傳輸效率。但是和通常的二進制文件格式一樣,為節省空間付出的代價就是犧牲了部分的可讀性,就像linus對systemd中二進制文件的看法一樣“I dislike the binary logs, for example”。轉換為二進制的message文件同樣存在着不直觀的問題,所以此時需要通過工具來講它轉換為文本格式——例如json格式——的文本以便於閱讀。在這個時候,protobuf生成代碼中生成的meta、Descriptor、scheme等格式就可以排上用處了。這一點在之前的分析中其實並沒有注意到它們在可視化讀取中的意義,只是注意到它在修改變量值的時候意義不太大。
二、從字符串反序列化出FileDescriptorProto內存對象
考慮一個文件的json格式化輸出,需要一個比較關鍵的就是每個字段的字符串名稱,這個字段是json輸出中最為關鍵也最為基礎的一個信息。首先第一個問題是,這個字段的字符串格式名稱從哪里來?
在為proto文件生成的c++代碼中,其中可以看到一些比較長的字符串結構,這些是查看生成的源文件中最為醒目的一個數據,在這個字符串中可以看到Message中各個字段的字符串格式的名稱,所以推測這些字符串的名稱從這里來。查看protobuf的源代碼可以看到,這個猜測是正確的,這個字符串其實也是一個protobuf生成的Message通過二進制格式化之后的內容。既然說它是一個Message二進制化之后的內容,所以這個protobuf應該有一個對應的proto文件,這個文件就是源代碼中存在的descriptor.proto文件。從這個文件中要注意到一個細節,就是其中對於字段名稱的定義是string格式的,另外關心的數值為number字段,還有一個是這個字段的類型。
也就是說,對於
message mainmsg
{
int32 x = 10;
}
這種格式,它在Descriptor中,其name為字符串形式的"x",number為數值形式的10,而Type則為枚舉的TYPE_INT32。
這個其實已經非常接近了json輸出中必須的字符串格式名字。
或者更為直觀的說,在生成文件中的字符串是一個FileDescriptorProto格式的Message實例,更精確的說,就是可以通過 descriptor.proto生成的對應代碼,直接調用它的接口,從這個字符串生成一個對應的實例,而這個實例完整的表示了proto文件的定義。事實上,對於用戶中的這個內容,在protobuf中也的確是通過這樣的接口來完成根據這些字符串來獲得這個消息的proto文件對應的內存對象。
在protobuf中對應的將生成源代碼中字符串形式的FileDescriptorProto轉換為內存對象的代碼為下面函數,可以看到,這個地方是直接從字符串反序列化出來一個FileDescriptorProto內存對象。在FileDescriptorProto類中,包含了完整的原始proto文件定義時都是可以查詢到的。
protobuf-master\src\google\protobuf\descriptor_database.cc
bool EncodedDescriptorDatabase::Add(
const void* encoded_file_descriptor, int size) {
FileDescriptorProto file;
if (file.ParseFromArray(encoded_file_descriptor, size)) {
return index_.AddFile(file, std::make_pair(encoded_file_descriptor, size));
} else {
GOOGLE_LOG(ERROR) << "Invalid file descriptor data passed to "
"EncodedDescriptorDatabase::Add().";
return false;
}
}
每個文件對應的FileDescriptorProto對象,通過C++的全局靜態變量在main函數運行前已經注冊到protobuf中。通過這個對象,可以找到原始文件中定義的所有消息、文件選項、每個消息的各個字段,這樣就有了最為原始的信息。
三、從FileDescriptorProto到FileDescriptor
FileDescriptorProto是從生成代碼中的字符串直接發序列化生成的內存對象,這個對象包含的是原始信息,但是這個結構也有一些局限性。這里我們關心的一個重要問題就是它只是proto文件的原始定義,並沒有我們最為關系的內存布局信息。這里所謂的內存布局是指消息中各個成員(Field)在一個Message對象中的偏移位置。因為proto會在生成的Message對象中添加一些內部結構,這些包括可控和不可控的字段,例如虛函數指針這些不可控字段、是否包含可選字段的bits標志位這種可控字段。所以在各種原始的XXXProto后綴的基礎上生成對應的無后綴結構。例如FileDescriptorProto對應的FileDescriptor、DescriptorProto對應的Descriptor、FieldDescriptorProto對應的FieldDescriptor。這些不帶Proto后綴的類雖然看起來是自動生成的,但事實上並不是,它們是根據對應的Proto文件手動構建(Build)出來的,這些Build的代碼主要位於protobuf-master\src\google\protobuf\descriptor.cc。例如
// These methods all have the same signature for the sake of the BUILD_ARRAY
// macro, below.
void BuildMessage(const DescriptorProto& proto,
const Descriptor* parent,
Descriptor* result);
void BuildField(const FieldDescriptorProto& proto,
const Descriptor* parent,
FieldDescriptor* result)
在這些Descriptor類中,和對應的DescriptorProto類相比,一個明顯的、我們感興趣的成員就是index()接口,這個接口相當於為每個用戶定義的結構分配了唯一的編號,並且這個編號是連續的,再更准確的說,這個字段是一個數組的下標,這樣通過循環來遍歷也非常方便。這一步非常重要,因為它完成了從字符串到數值的綁定關系。
四、從FileDescriptor到File
在json轉換過程中,更簡潔的是不帶Descriptor后綴的表示形式,對應於FieldDescriptor它的內容為Field,其同樣是通過proto文件定義,位於中。整個表達更加簡潔,其中比較基礎的依然是類型、編號、名字三個字段,有這三個字段其實就可以完成對於protobuf中TLV內容的解析了,這個轉換在protobuf-master\src\google\protobuf\util\type_resolver_util.cc中完成
oid ConvertFieldDescriptor(const FieldDescriptor* descriptor, Field* field) {
field->set_kind(static_cast<Field::Kind>(descriptor->type()));
switch (descriptor->label()) {
case FieldDescriptor::LABEL_OPTIONAL:
field->set_cardinality(Field::CARDINALITY_OPTIONAL);
break;
case FieldDescriptor::LABEL_REPEATED:
field->set_cardinality(Field::CARDINALITY_REPEATED);
break;
case FieldDescriptor::LABEL_REQUIRED:
field->set_cardinality(Field::CARDINALITY_REQUIRED);
break;
}
field->set_number(descriptor->number());
field->set_name(descriptor->name());
field->set_json_name(descriptor->json_name());
……
}
五、json格式的輸出
從TLV中解析出來Tag之后,可以找到對一個的Field,通過Field中的type知道基本類型,通過name知道字符串名稱,這個其實已經完成了解析的必備條件。
src\google\protobuf\util\internal\protostream_objectsource.cc
Status ProtoStreamObjectSource::RenderNonMessageField(
const google::protobuf::Field* field, StringPiece field_name,
ObjectWriter* ow) const {
// Temporary buffers of different types.
uint32 buffer32;
uint64 buffer64;
std::string strbuffer;
switch (field->kind()) {
case google::protobuf::Field_Kind_TYPE_BOOL: {
stream_->ReadVarint64(&buffer64);
ow->RenderBool(field_name, buffer64 != 0);
break;
}
六、從index到offset
當有了唯一的index編號之后,就可以使用這個作為下標來所以內存偏移量,這個在生成的CPP文件中的名字就是offsets。當運行的時候,這個信息保存在了ReflectionSchema對象中,這里最為關鍵的就是其中的offsets_字段,它可以通過Field的index索引找到該Field在一個對象中的偏移量。有了這些信息,就可以結合通過一個Message對象的指針,加上某個Field中保存的offset字段,就可以定位到它在內存中的位置。
protobuf-master\src\google\protobuf\generated_message_reflection.cc
// Helper function to transform migration schema into reflection schema.
ReflectionSchema MigrationToReflectionSchema(
const Message* const* default_instance, const uint32* offsets,
MigrationSchema migration_schema) {
ReflectionSchema result;
result.default_instance_ = *default_instance;
// First 6 offsets are offsets to the special fields. The following offsets
// are the proto fields.
result.offsets_ = offsets + migration_schema.offsets_index + 5;
result.has_bit_indices_ = offsets + migration_schema.has_bit_indices_index;
result.has_bits_offset_ = offsets[migration_schema.offsets_index + 0];
result.metadata_offset_ = offsets[migration_schema.offsets_index + 1];
result.extensions_offset_ = offsets[migration_schema.offsets_index + 2];
result.oneof_case_offset_ = offsets[migration_schema.offsets_index + 3];
result.object_size_ = migration_schema.object_size;
result.weak_field_map_offset_ = offsets[migration_schema.offsets_index + 4];
return result;
}
七、offset從哪里來
MigrationToReflectionSchema函數中傳入參數中的offsets和前面提到的用戶自定義proto文件一樣以常量的形式保存在生成的cpp文件中。
八、舉個栗子
tsecer@harry :cat msgdef.proto
syntax = "proto3";
message subsubmsg
{
int32 x = 1;
};
message submsg
{
subsubmsg x = 1;
float y = 2;
};
message mainmsg
{
submsg msg = 3;
int32 x = 1;
float y = 2;
};
tsecer@harry :protoc --cpp_out=. msgdef.proto
tsecer@harry :
這個proto文件對應的FileDescriptorProto類型對象進行序列化之后的內容為:
const char descriptor_table_protodef_msgdef_2eproto[] =
"\n\014msgdef.proto\"\026\n\tsubsubmsg\022\t\n\001x\030\001 \001(\005\"*"
"\n\006submsg\022\025\n\001x\030\001 \001(\0132\n.subsubmsg\022\t\n\001y\030\002 \001"
"(\002\"5\n\007mainmsg\022\024\n\003msg\030\003 \001(\0132\007.submsg\022\t\n\001x"
"\030\001 \001(\005\022\t\n\001y\030\002 \001(\002b\006proto3"
;
這個內容可以直接反序列化出來一個FileDescriptorProto對象。
const ::PROTOBUF_NAMESPACE_ID::uint32 TableStruct_msgdef_2eproto::offsets[] PROTOBUF_SECTION_VARIABLE(protodesc_cold) = {
~0u, // no _has_bits_
PROTOBUF_FIELD_OFFSET(::subsubmsg, _internal_metadata_),
~0u, // no _extensions_
~0u, // no _oneof_case_
~0u, // no _weak_field_map_
PROTOBUF_FIELD_OFFSET(::subsubmsg, x_),
~0u, // no _has_bits_
PROTOBUF_FIELD_OFFSET(::submsg, _internal_metadata_),
~0u, // no _extensions_
~0u, // no _oneof_case_
~0u, // no _weak_field_map_
PROTOBUF_FIELD_OFFSET(::submsg, x_),
PROTOBUF_FIELD_OFFSET(::submsg, y_),
~0u, // no _has_bits_
PROTOBUF_FIELD_OFFSET(::mainmsg, _internal_metadata_),
~0u, // no _extensions_
~0u, // no _oneof_case_
~0u, // no _weak_field_map_
PROTOBUF_FIELD_OFFSET(::mainmsg, msg_),
PROTOBUF_FIELD_OFFSET(::mainmsg, x_),
PROTOBUF_FIELD_OFFSET(::mainmsg, y_),
};
static const ::PROTOBUF_NAMESPACE_ID::internal::MigrationSchema schemas[] PROTOBUF_SECTION_VARIABLE(protodesc_cold) = {
{ 0, -1, sizeof(::subsubmsg)},
{ 6, -1, sizeof(::submsg)},
{ 13, -1, sizeof(::mainmsg)},
};
上面MigrationSchema三個元素分別表示了三個消息在TableStruct_msgdef_2eproto::offsets數組中的起始下標編號(以及各自的結構大小)。在TableStruct_msgdef_2eproto::offsets內部,開始5個為預定義內部結構,從第六個開始為各個字段在對象中的偏移位置,這個偏移位置可以通過前面提到的FieldDescriptor中的index()作為下標訪問。
下面offsets_[field->index()]將前面的TableStruct_msgdef_2eproto::offsets和index連接起來
protobuf-master\src\google\protobuf\generated_message_reflection.h
// Offset of a non-oneof field. Getting a field offset is slightly more
// efficient when we know statically that it is not a oneof field.
uint32 GetFieldOffsetNonOneof(const FieldDescriptor* field) const {
GOOGLE_DCHECK(!field->containing_oneof());
return OffsetValue(offsets_[field->index()], field->type());
}
九、index從哪里來
這個其實比較簡單,就是按照聲明的順序依次編碼即可獲得。
protobuf-master\src\google\protobuf\descriptor.h
// To save space, index() is computed by looking at the descriptor's position
// in the parent's array of children.
inline int FieldDescriptor::index() const {
if (!is_extension_) {
return static_cast<int>(this - containing_type()->fields_);
} else if (extension_scope_ != NULL) {
return static_cast<int>(this - extension_scope_->extensions_);
} else {
return static_cast<int>(this - file_->extensions_);
}
}
十、遍歷打印簡單結構
通過Message的Descriptor和Reflection遍歷打印結構,這里只考慮了最簡單的INT32和FLOAT及Message消息類型
tsecer@harry :cat main.cpp
#include "stdio.h"
#include "msgdef.pb.h"
#include <stdio.h>
using namespace google::protobuf;
void printmsg(const Message &mmsg, int indent)
{
const Descriptor* pstDesc = mmsg.GetDescriptor();
const Reflection* pstRefl = mmsg.GetReflection();
printf("\n%*c %s", indent * 10, ' ', pstDesc->name().c_str());
for (int i = 0; i < pstDesc->field_count(); i++)
{
printf("%*c", indent * 10, ' ');
const FieldDescriptor *pstFieldDesc = pstDesc->field(i);
switch (pstFieldDesc->cpp_type())
{
case FieldDescriptor::CPPTYPE_INT32 : printf("%s\t%d\t", pstFieldDesc->name().c_str(), pstRefl->GetInt32(mmsg, pstFieldDesc)); break;
case FieldDescriptor::CPPTYPE_FLOAT : printf("%s\t%f\t", pstFieldDesc->name().c_str(), pstRefl->GetFloat(mmsg, pstFieldDesc)); break;
default:
printmsg(pstRefl->GetMessage(mmsg, pstFieldDesc), indent + 1);
}
}
printf("\n");
}
int main()
{
mainmsg mmsg;
mmsg.set_x(11);
mmsg.set_y(2.2);
mmsg.mutable_msg()->set_y(3.3);
mmsg.mutable_msg()->mutable_x()->set_x(4.4);
printmsg(mmsg, 0);
return 0;
}
tsecer@harry :make
g++ -std=c++11 msgdef.pb.cc main.cpp -lprotobuf -g
tsecer@harry :./a.out
mainmsg
submsg
subsubmsg x 4
y 3.300000
x 11 y 2.200000
tsecer@harry :