Protobuf-源码中值得学习的地方

使用宏来提高代码可读性(代码的美感)

例1.宏CHARACTER_CLASS

定义

tokenizer.cc文件中,需要判断某个character是属于哪种类型的字符,通过宏CHARACTER_CLASS来定义字符类型,并且定义static类型的InClass()接口来判断。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#define CHARACTER_CLASS(NAME, EXPRESSION)      \
class NAME { \
public: \
static inline bool InClass(char c) { \
return EXPRESSION; \
} \
}

CHARACTER_CLASS(Whitespace, c == ' ' || c == '\n' || c == '\t' ||
c == '\r' || c == '\v' || c == '\f');

CHARACTER_CLASS(Unprintable, c < ' ' && c > '\0');

CHARACTER_CLASS(Digit, '0' <= c && c <= '9');
CHARACTER_CLASS(OctalDigit, '0' <= c && c <= '7');
CHARACTER_CLASS(HexDigit, ('0' <= c && c <= '9') ||
('a' <= c && c <= 'f') ||
('A' <= c && c <= 'F'));

CHARACTER_CLASS(Letter, ('a' <= c && c <= 'z') ||
('A' <= c && c <= 'Z') ||
(c == '_'));

CHARACTER_CLASS(Alphanumeric, ('a' <= c && c <= 'z') ||
('A' <= c && c <= 'Z') ||
('0' <= c && c <= '9') ||
(c == '_'));

CHARACTER_CLASS(Escape, c == 'a' || c == 'b' || c == 'f' || c == 'n' ||
c == 'r' || c == 't' || c == 'v' || c == '\\' ||
c == '?' || c == '\'' || c == '\"');

#undef CHARACTER_CLASS

使用

使用时可以直接使用InClass():

1
2
3
4
5
6
7
8
9
template<typename CharacterClass>
inline bool Tokenizer::TryConsumeOne() {
if (CharacterClass::InClass(current_char_)) {
NextChar();
return true;
} else {
return false;
}
}

例2.宏 PROTOBUF_DEFINE_ACCESSOR

FieldDescriptor类,因为可能类型是多样的,在实现对外暴露default数据的函数时,为了提高代码可读性,使用了如下宏的方式(文件descriptor.cc中):

定义

1
2
3
// These macros makes this repetitive code more readable.
#define PROTOBUF_DEFINE_ACCESSOR(CLASS, FIELD, TYPE) \
inline TYPE CLASS::FIELD() const { return FIELD##_; }

使用

1
2
PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, default_value_int32 , int32 )
PROTOBUF_DEFINE_ACCESSOR(FieldDescriptor, has_default_value, bool)

例3.宏BUILD_ARRAY

定义

BUILD_ARRAY宏定义如下,这里的INPUT是proto;OUTPUT是proto对应的descriptor;NAME是需要完成创建的成员;METHOD是创建descriptor成员时需要调用的函数;PARENT是发生嵌套时的上一级。

1
2
3
4
5
6
7
8
   // A common pattern:  We want to convert a repeated field in the descriptor
// to an array of values, calling some method to build each value.
#define BUILD_ARRAY(INPUT, OUTPUT, NAME, METHOD, PARENT) \
OUTPUT->NAME##_count_ = INPUT.NAME##_size(); \
AllocateArray(INPUT.NAME##_size(), &OUTPUT->NAME##s_); \
for (int i = 0; i < INPUT.NAME##_size(); i++) { \
METHOD(INPUT.NAME(i), PARENT, OUTPUT->NAME##s_ + i); \
}

使用

DescriptorBuilder::BuildFile()中,利用FileDescriptorProto& proto来构建对应的descriptor:

1
2
3
4
5
// Convert children.
BUILD_ARRAY(proto, result, message_type, BuildMessage , NULL);
BUILD_ARRAY(proto, result, enum_type , BuildEnum , NULL);
BUILD_ARRAY(proto, result, service , BuildService , NULL);
BUILD_ARRAY(proto, result, extension , BuildExtension, NULL);

说明

各个Descriptor类中,使用count + 连续内存来保存成员,例如:

1
2
3
4
5
6
7
8
9
10
class LIBPROTOBUF_EXPORT FileDescriptor {

//省略其它代码

int message_type_count_;
Descriptor* message_types_;

//省略其它代码

}

资源分配/处理的lazy机制

例1.类DescriptorPool数据分层设计

DescriptorPool的数据管理分为了多层(忽略了仅在protobuf内部使用 && 不推荐使用的underlay一层):

  • 最顶层:DescriptorPool::Tables tables_,保存name->descriptor;
  • 最底层:DescriptorDatabase* fallback_database_,保存name->file_descriptor_proto(而不是直接的file_descriptor)

查找时,如果第一层tables_没找到,最终会到fallback_database_中找对应proto,并且调用临时构造的DescriptorBuilder::Build*()系列接口把生成的descriptor添加到tables_中,然后再从tables_中找。

这样数据分层设计的目的是:

  1. 用于定制地(on-demand)从某种”大”的database加载产生DescriptorPool。因为database太大,逐个调用DescriptorPool::BuildFile() 来处理原database中的每一个proto文件是低效的。
    为了提升效率,使用DescriptorPool来封装DescriptorDatabase,并且只建立正真需要的descriptor。
  2. 针对编译依赖的每个proto文件,并不是在进程启动时,直接构建出proto中所包含的所有descriptor,而是hang on,直到某个descriptor真的被需要:
    (1)用户调用例如descriptor(), GetDescriptor(), GetReflection()的方法,需要返回descriptor;
    (2)用户从DescriptorPool::generated_pool()中查找descriptor;

可以看到descriptor的构建是hang-on的,只有需要使用某个descriptor时,才构建。适合依赖了很多的proto文件,但仅仅使用其中的少数proto的场景。

例2.类GeneratedMessageFactory映射关系加载

GeneratedMessageFactory类,管理的从Descriptor* -> Message*映射关系,并不是一开始就注册好的。仅仅在需要从descriptr查找message时(调用GeneratedMessageFactory::GetPrototype()),才会:

  1. 通过file_name找到注册函数;
  2. 调用注册函数,完成Descriptor* -> Message*映射关系的注册;
  3. hash_map<const Descriptor*, const Message*> type_map_查找到对应Message*返回;

资源管理/内存复用

类RepeatedPtrFieldBase

RepeatedPtrFields的父类(不是模板类,提供了多个模板函数),本身保存/管理的数据类型为void*(message对象的实际地址,也是通过连续内存array来保存)。

因为array中保存的是同一个descriptor对应的message,只是各个message中所包含的数据不一样,为了节省下message对象分配/释放的成本,所以message可以被clear(clear操作会将primitive类型的field设置为0,其余类型field调用自身的clear()接口处理,类似std::string::clear(),只清理数据并不回收内存)。
然后保留原有的内存地址在array中。下次需要从array中分配message时,优先使用这一批被clear的message(实现在RepeatedPtrFieldBase::AddFromCleared() ,参考GeneratedMessageReflection::AddMessage()中的调用方式)。

为了管理cleared状态的message指针,引入了多个游标来标记数据:

  • current_size_: 当前待处理的message地址;
  • allocated_size_:已经分配message的数据,current_size_ <= allocated_size_,从current_size_allocated_size_之间的message就是被cleared的;
  • total_size_:elements_[]的长度,但从allocated_size_total_size_之间的void*是无效的,并没有指向任何message;

对应内存分布如下图所示:

avatar

封装多种类型,统一对外的服务

针对数据/行为简单的类型,使用轻量级的方案(struct/enum/union/switch-case),来实现类型的封装,而不是采用继承方式来实现。

Symbol可能有多种类型,enum Type表示具体类型,union让多种类型都复用同一个内存地址:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
struct Symbol {
enum Type {
NULL_SYMBOL, MESSAGE, FIELD, ENUM, ENUM_VALUE, SERVICE, METHOD, PACKAGE
};
Type type;
union {
const Descriptor* descriptor;
const FieldDescriptor* field_descriptor;
const EnumDescriptor* enum_descriptor;
const EnumValueDescriptor* enum_value_descriptor;
const ServiceDescriptor* service_descriptor;
const MethodDescriptor* method_descriptor;
const FileDescriptor* package_file_descriptor;
};

inline Symbol() : type(NULL_SYMBOL) { descriptor = NULL; }
…… //省略部分

CONSTRUCTOR帮助提高代码可读性,来实现不同类型Symbol的构造函数:

1
2
3
4
5
#define CONSTRUCTOR(TYPE, TYPE_CONSTANT, FIELD)  \
inline explicit Symbol(const TYPE* value) { \
type = TYPE_CONSTANT; \
this->FIELD = value; \
}

CONSTRUCTOR的使用:

1
2
3
4
5
6
7
8
  CONSTRUCTOR(Descriptor         , MESSAGE   , descriptor             )
CONSTRUCTOR(FieldDescriptor , FIELD , field_descriptor )
CONSTRUCTOR(EnumDescriptor , ENUM , enum_descriptor )
CONSTRUCTOR(EnumValueDescriptor, ENUM_VALUE, enum_value_descriptor )
CONSTRUCTOR(ServiceDescriptor , SERVICE , service_descriptor )
CONSTRUCTOR(MethodDescriptor , METHOD , method_descriptor )
CONSTRUCTOR(FileDescriptor , PACKAGE , package_file_descriptor)
#undef CONSTRUCTOR

具体应用时,根据type来区分处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
  const FileDescriptor* GetFile() const {
switch (type) {
case NULL_SYMBOL: return NULL;
case MESSAGE : return descriptor ->file();
case FIELD : return field_descriptor ->file();
case ENUM : return enum_descriptor ->file();
case ENUM_VALUE : return enum_value_descriptor->type()->file();
case SERVICE : return service_descriptor ->file();
case METHOD : return method_descriptor ->service()->file();
case PACKAGE : return package_file_descriptor;
}
return NULL;
}
};

不同的类作为模版参数时,提供类独有的类型

GenericTypeHandler和类StringTypeHandler 需要作为模版类型参数(typehandler),在子类RepeatedPtrField在调用父类RepeatedPtrFieldBase的模板函数时,通过模板参数直接传入父类RepeatedPtrFieldBase,这里需要根据不同的typehandler,返回对应不同的类型:

1
2
3
4
5
6
template <typename TypeHandler>
inline const typename TypeHandler::Type&
RepeatedPtrFieldBase::Get(int index) const {
GOOGLE_DCHECK_LT(index, size());
return *cast<TypeHandler>(elements_[index]);
}

所以有如下方式,在不同模版参数类型中通过typedef方式来实现类型名称的统一,因为对于模版来说,关键点就是有统一的名称。

GenericTypeHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
template <typename GenericType>
class GenericTypeHandler {
public:
typedef GenericType Type;

static GenericType* New() { return new GenericType; }
static void Delete(GenericType* value) { delete value; }
static void Clear(GenericType* value) { value->Clear(); }
static void Merge(const GenericType& from, GenericType* to) {
to->MergeFrom(from);
}
static int SpaceUsed(const GenericType& value) { return value.SpaceUsed(); }
};

StringTypeHandler

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
// HACK:  If a class is declared as DLL-exported in MSVC, it insists on
// generating copies of all its methods -- even inline ones -- to include
// in the DLL. But SpaceUsed() calls StringSpaceUsedExcludingSelf() which
// isn't in the lite library, therefore the lite library cannot link if
// StringTypeHandler is exported. So, we factor out StringTypeHandlerBase,
// export that, then make StringTypeHandler be a subclass which is NOT
// exported.
// TODO(kenton): There has to be a better way.

class LIBPROTOBUF_EXPORT StringTypeHandlerBase {
public:
typedef string Type;
static string* New();
static void Delete(string* value);
static void Clear(string* value) { value->clear(); }
static void Merge(const string& from, string* to) { *to = from; }
};

class LIBPROTOBUF_EXPORT StringTypeHandler : public StringTypeHandlerBase {
public:
static int SpaceUsed(const string& value) {
return sizeof(value) + StringSpaceUsedExcludingSelf(value);
}
};

对应类的关系图:

avatar

低配版release来节省资源

在proto文件中增加配置,产出不支持reflection/descriptor的MessageLite子类,而不是Message子类。

1
option optimize_for = LITE_RUNTIME