protobuf 之 Custom Options

#protobuf

1. 问题

protobuf 默认支持的数据类型有 double float int32 int64 string bytes bool 等几种类型。在有的生产场景中,我们可能需要更多的类型,比如把 protobuf 转换为 mcpack(厂内某个很古老的数据格式),对于一段字符串,protobuf 统一认为是 string/bytes,而 mcpack 则把字符串区分为 raw/string 两种类型,此时就需要我们在 proto 能够标记以区分这两种类型。在 brpc 的 mcpack2pb里也是类似的问题。

或者是数据存储/分发的场景,对于接收的 message,我们希望有的字段能够覆盖写、有的删除、有的建索引,如果在定义 message 的时候就能够提前约定,策略同学在新增字段的时候就可以直接实现对存储的预期。

protobuf 的 Custom Options特性可以实现这点。

2. descriptor

protobuf 里实现了众多的Descriptor类型,例如Descriptor对应具体的 message,FieldDescriptor对应某一个字段,EnumDescriptorFileDescriptor,ServiceDescriptor等都对应了 protobuf 里的某个”实体”,用于支持我们获取其描述性的结构性质。

对于每种xxxDescriptor,在descritpor.proto都有相应的 Options,例如MessageOptions FieldOptions EnumOptions ServiceOptions,这些都定义为普通的 pb message.

这些类之间的关系,具体的,举个例子:

class LIBPROTOBUF_EXPORT Descriptor {
    ...
    const MessageOptions& options() const;
    ...
    // The number of fields in this message type.
    int field_count() const;
    // Gets a field by index, where 0 <= index < field_count().
    // These are returned in the order they were defined in the .proto file.
    const FieldDescriptor* field(int index) const;

class LIBPROTOBUF_EXPORT FieldDescriptor {
    ...
    const FieldOptions& options() const;

我们可以通过Descriptor获取到字段的FieldOptions,而 Custom Options 的奥秘,就来自于对这些自定义 message options 的扩展,接下来,看个具体extend FieldOptions的示例,其他也是类似。

3. extend FieldOptions 表示不同类型

FieldOptions定义为 message:

message FieldOptions {
    ...
    optional bool deprecated = 3 [default=false];
    ...
    // Clients can define custom options in extensions of this message. See above.
    extensions 1000 to max;
}

我们通过扩展FieldOptions指定该 field 类型是与 pb 原类型语义相同(ORIGINAL),还是需要指定为 RAW 类型(针对 RAW 类型,我们需要有不同的处理方式)。

enum CustomFieldType {
    RAW = 0;
    ORIGINAL = 1;
}

extend google.protobuf.FieldOptions {
    optional CustomFieldType custom_field_type = 50002 [default = ORIGINAL];
}

注意这里只是简单的 extensions,因此可以正常使用GetExtension等接口获取该值。

接下来我们定义自己的 message:

message MyMessage {
    optional int32 old_field = 1 [deprecated=true];
    optional int32 new_field = 2;
    optional string name = 3;
    optional string raw_str = 4 [(custom_field_type) = RAW];
}

old_field new_field用于对比FieldOptions预定义的deprecated.
raw_str则定义了(custom_field_type) = RAW,由于该值默认为ORIGINAL,定义了name用于对比。

接下来就是具体使用的例子:

    const ::google::protobuf::Descriptor* descriptor = MyMessage::descriptor();
    for (int i = 0; i < descriptor->field_count(); ++i) {
        const ::google::protobuf::FieldDescriptor* field_descriptor = descriptor->field(i);
        const ::google::protobuf::FieldOptions& field_options = field_descriptor->options();
        std::cout << "FieldName:" << field_descriptor->full_name()
            << "\tCustomFieldType:" << field_options.GetExtension(custom_field_type)
            << "\tDeprecated:" << field_options.deprecated() << std::endl;
    }

输出为:

FieldName:MyMessage.old_field   CustomFieldType:1       Deprecated:1
FieldName:MyMessage.new_field   CustomFieldType:1       Deprecated:0
FieldName:MyMessage.name        CustomFieldType:1       Deprecated:0
FieldName:MyMessage.raw_str     CustomFieldType:0       Deprecated:0

可以看到,通过这种方式,即使都是optional string类型,我们也可以通过自定义的 options,来区分更细的不同类型(这里是 RAW),之后执行不同的操作,例如对于普通的 string 类型,mcpack 是put_str/get_str,而对于 RAW,则需要put_raw/get_raw

deprecated作为message FieldOptions的预定义 field,可以直接使用deprecated()接口,而custom_field_type作为 extend 的字段,需要使用GetExtension()接口,这点与普通 message 是一致的。

4. TIPS

自定义的类型使用时需要使用(...)括起来,以跟预定义 option field 区分。扩展的字段范围可以直接参考 descriptor.proto,大部分是extensions 1000 to max;,跟普通的 extensions 一样,在项目里尽量避免冲突。