本文主要介绍Protocol Buffer类在C++如何工作,当然也包含Protocol Buffer本身应用文档。本文包含:

  • 通过Protocol Buffer编译器,如何从.proto文件中生成C++代码。
  • 为提供的源码生成API文档。

当然也有其它语言对应的API引用文档,详见这里

代码生成指南

编译器调用

消息
字段
Any
Oneof
枚举
扩展
Arena分配
服务
插件

本章节实际上是介绍Protocol Buffer编译器从给定的protocol定义中生成的C++代码。所有proto2和proto3生成的代码不同之处都会高亮标出 — 需要注意的是这些不同之处只是生成的代码中的不同,而不是消息类/接口(同一版本的编译器生成的是一样的)的不同。开始之前,你应该先了解proto2 language guideproto3 language guide

编译器调用

使用--cpp_out=命令行参数,Protocol Buffer编译器会生成C++输出。--cpp_out=选项的参数是你要存放C++输出的目录。编译器会为每个.proto文件生成一个头文件和实现文件。输出文件的名称与给定的.proto文件名称有关:

  • 后缀(.proto)被替换成.pb.h(头文件)或pb.cc(实现文件)。
  • proto路径(通过--proto_path-I指定)被输出路径(通过--cpp_out指定)替换。

例如,调用如下命令:

protoc –proto_path=src –cpp_out=build/gen src/foo.proto src/bar/baz.proto

编译器读取文件src/foo.protosrc/bar/baz.proto并产生4个输出文件:build/gen/foo.pb.hbuild/gen/foo.pb.ccbuild/gen/bar/baz.pb.hbuild/gen/bar/baz.pb.cc。需要的话,编译器会自动生成build/gen/bar目录,但是并不会创建buildbuild/gen,因此,它们必须已存在。

如果.proto文件包含package声明,那么文件中所有的内容都会被放在对应的C++命名空间中。例如,给定package声明:

pakcage foo.bar

文件中的所有声明都会放在foo::bar命名空间中。

消息

如下,是一个简单的消息声明:

message Foo {}

编译器会生成一个名为Foo的类,派生自google::protobuf::Message。这个类是一个具体的类,不存在为实现的纯虚函数。取决与优化模式,Message中的虚函数会/不会被Foo重写。默认情况下,Foo实现所有方法的特定版本以获得最快速度。但是,如果.proto文件中包含:

option optimize_for = CODE_SIZE;

之后Foo只重写功能所需的最小方法集,剩下的靠基本的反射实现。这会显著减小生成代码的大小,但会降低性能。或者,如果.proto文件中包含:

option optimize_for = LITE_RUNTIME;

之后Foo会包含所有方法的快速实现,但实现的是google::protobuf::MessageLite的接口,它只是Message方法的一个子集。需要特意说明的是,它不支持描述符或反射。但是,这种模式下,生成的代码只需链接libprotobuf-lite.so(Windows下libprotobuf-lite.lib)即可,而不是libprotobuf.solibprotobuf.lib)。“lite”版本的库比完整的库要小的多,特别适合像手机这样的资源有限的系统。

应该创建自己的Foo子类。如果你创建了子类且重写了虚函数,重写的函数可能会被忽略,因为许多生成的方法调用被去虚拟胡以提高性能。

Message接口定义了可以让你检查、操作、读写整个消息的方法,包括从二进制字符串中解析和序列化为二进制字符串。

  • bool ParseFromString(const string& data):从给定的序列化后的二进制字符串(即wire格式)解析消息。
  • bool SerializeToString(string* output):将给定的消息序列化为二进制字符串。
  • string DebugString():返回字符串,文本格式的proto表述(只应在debugging时使用)。

作为上述方法的补充,Foo类定义了下列方法:

  • Foo():默认构造函数。
  • ~Foo():默认析构函数。
  • Foo(const Foo& other):拷贝构造。
  • Foo& operator=(const Foo& other):赋值运算符。
  • void Swap(Foo* other):与另一消息交换信息。
  • const UnknownFieldSet& unknown_fields() const:返回解析消息遇到的未知字段的集合。

Foo类还定义了下面的静态方法:

  • static const Descriptor* descriptor():返回类型的描述,包含该类型的信息,包括有什么字段以及它们的类型。用于反射时,可以以编程的方式来检查字段。
  • static const Foo& default_instance():返回一个单例模式的Foo实例,它与新构造的Foo实例相同(所以所有的单个字段都是未设置的,所有的重复字段都是空的)。。注意,通过调用New()方法,消息的默认实例可以当作工厂使用。

可以在一个消息中声明另一个消息,就像message Foo { message Bar { } }

这种情况下,编译器会生成两个类:FooFoo_Bar。额外地,编译器会在Foo类中生成如下的typedef:

typedef Foo_Bar Bar;

这意味着你可以像使用内嵌类Foo::Bar那样使用内嵌类型的类。但是,注意C++不允许内嵌类型被前向声明。如果要在另一个文件中使用前向声明Bar并使用该声明,则必须将其标识为Foo_Bar

字段

补充之前的章节,Protocol Buffer编译器会为.proto文件中定义的每个字段生成一系列的访问方法。

与访问方法一样,编译器为每个包含包含其字段序号的字段生成一个整数常量。常量名是字母k,后跟转换成首字母大写的字段名,之后是FieldNumber。例如,给定字段optional int32 foo_bar = 5;,编译器会生成常量static const int kFooBarFiledNumber = 5;

对于返回const引用的字段访问器,在调用另一个修改访问器修改消息时,该引用会被调用。这包括调用字段的任意非const访问器,从Message继承的任意非const方法或其它修改修改消息的方法(比如,作为Swap()的参数使用)。相应地,如果在此期间没有对消息进行修改访问,则仅保证在不同的访问方法中返回的引用的地址是相同的。

对于返回指针的字段访问器,在对消息的下一次修改/不修改时,指针可能会失效。这包括调用任何字段的任意访问器、从Message继承的任意方法或通过其它方式访问消息(比如,使用拷贝构造拷贝消息)。相应地,在访问器的两次不同调用之间,返回的指针的值永远不能保证相同。

单个数值字段

对于下面的定义:

int32 foo = 1;

编译器会生成如下方法:

  • int32 foo() const:返回字段目前的值。如果字段未设置,返回0。
  • void set_foo(int32 value):设置字段的值。调用之后,foo()会返回value
  • void clear_foo():清空字段的值。调用之后,foo()将返回0。

对于其他数值字段类型(包括bool),根据标量值类型表,int32被相应的c++类型替换。

单个字符串字段

对于任意下面这些定义之一:

string foo = 1; bytes foo = 1;

编译器会生成如下方法:

  • const string& foo() const:返回字段当前的值。如果字段未设置,则返回空string/bytes。
  • void set_foo(const string& value):设置字段的值。调用之后,foo()将返回value的拷贝。
  • void set_foo(string&& value)(C++11及之后):设置字段的值,从传入的值中移入。调用之后,foo()将返回value的拷贝。
  • void set_foo(const char* value):使用C格式的空终止字符串设置字段的值。调用之后,foo()将返回value的拷贝。
  • void set_foo(const char* value, int size):如上,但使用的给定的大小而不是寻找空终止符。
  • string* mutable_foo():返回存储字段值的可变string对象的指针。若在字段设置之前调用,则返回空字符串。调用之后,foo()会将写入值返回给给定的字符串。
  • void clear_foo()::清空字段的值。调用之后,foo()将返回空string/bytes。
  • void set_allocated_oo(string* value):设置字段为给定string对象,若已存在,则释放之前的字段值。如果string指针非NULL,消息将获取已分配的string对象的所有权。消息可以在任何时候删除已分配的string对象,因此对该对象的引用可能无效。另外,若valueNULL,该操作与调用clear_foo()效果相同。
  • string* release_foo():释放字段的所有权并返回string对象的指针。调用之后,调用者将获得已分配的string对象的所有权,foo()将返回空string/bytes。

单个枚举字段

给出如下的枚举类型:

enum Bar {
  BAR_VALUE = 0;
  OTHER_VALUE = 1;
}

对于字段的定义:

Bar foo = 1;

编译器会生成如下方法:

  • Bar foo() const:返回字段当前的值。如果未设置,则返回默认值(0)。
  • void set_foo(Bar value):设置字段的值。调用之后,foo()将放回value
  • void clear_foo():清空字段的值。调用之后,foo()返回默认值。

单个内嵌消息字段

给出如下消息类型:

message Bar { }

对于如下定义:

Bar foo = 1;

编译器会生成如下方法:

  • bool has_foo() const:如果字段已设置,则返回true
  • const Bar& foo() const:返回字段当前的值。如果字段未设置,则返回一个未设置任何字段的Bar(也许是,Bar::default_instance())。
  • Bar* mutable_foo():返回存储字段值的可变Bar对象的指针。若在字段设置之前调用,则返回一个未设置任何字段的Bar(即,新分配的Bar对象)。调用之后,has_foo()会返回truefoo()返回一个与该实例相同的引用。
  • clear_foo():清空字段的值。调用之后,has_foo()会返回falsefoo()返回默认值。
  • void set_allocated_foo(Bar* bar):设置字段为给定bar对象,若已存在,则释放之前的字段值。如果Bar指针非NULL,消息将获取已分配的Bar对象的所有权且has_foo()会返回true。另外,若valueNULL,该操作与调用clear_foo()效果相同。
  • Bar* release_foo():释放字段的所有权并返回Bar对象的指针。调用之后,调用者将获得已分配的Bar对象的所有权且has_foo()会返回falsefoo()将返回默认值。

重复的数值字段

对于如下定义:

repeated int32 foo = 1;

编译器会生成如下方法:

  • int foo_size() const:返回字段中当前元素的数量。
  • int32 foo(int index) const:返回给定的从0开始索引的元素。使用超出[0,foo_size())范围的索引来调用该方法会受到未定义的行为。
  • void set_foo(int index, int32 value): 为给定的从0开始索引的元素赋值。
  • void add_foo(int32 value):将给定的值追加到字段中。
  • void clear_foo():移除字段的所有元素。调用之后,foo_size()将返回0。
  • const RepeatedField<int32>& foo() const:返回存储字段元素的基础RepeatedField。这个容器类提供了类似于STL的迭代器和其他方法。
  • RepeatedField<int32>* mutable_foo():返回存储字段元素的基础RepeatedField的指针。这个容器类提供了类似于STL的迭代器和其他方法。

对于其他数值字段类型(包括bool),根据标量值类型表,int32被相应的c++类型替换。

重复的字符串字段

对于任意下面这些定义之一:

string foo = 1; bytes foo = 1;

编译器会生成如下方法:

  • int foo_size() const:返回字段中当前元素的数量。
  • const string& foo(int index) const:返回给定的从0开始索引的元素。使用超出[0,foo_size())范围的索引来调用该方法会受到未定义的行为。
  • void set_foo(int index, const string& value):为给定的从0开始索引的元素赋值。
  • void set_foo(int index, const char* value):使用C风格的空终止符字符串为给定的从0开始索引的元素赋值。
  • void set_foo(int index, const char* value, int size):如上,但使用的给定的大小而不是寻找空终止符。
  • string* mutable_foo(int index):返回给定的从0开始索引的元素所存储的可变string对象的指针。使用超出[0,foo_size())范围的索引来调用该方法会受到未定义的行为。
  • void add_foo(const string& value):使用给定的值为字段追加一个新元素。
  • void add_foo(const char* value):使用给定的C风格的空终止符字符串为字段追加一个新元素。
  • void add_foo(const char* value, int size):如上,但使用的给定的大小而不是寻找空终止符。
  • string* add_foo():新增一个空元素并返回它的指针。
  • void clear_foo():移除字段的所有元素。调用之后,foo_size()将返回0。
  • const RepeatedField<string>& foo() const:返回存储字段元素的基础RepeatedField。这个容器类提供了类似于STL的迭代器和其他方法。
  • RepeatedField<string>* mutable_foo():返回存储字段元素的基础RepeatedField的指针。这个容器类提供了类似于STL的迭代器和其他方法。

重复的枚举字段

给出枚举类型:

enum Bar {
  BAR_VALUE = 0;
  OTHER_VALUE = 1;
}

定义如下:

repeated Bar foo = 1;

编译器会生成如下方法:

  • int foo_size() const:返回字段中当前元素的数量。
  • const Bar foo(int index) const:返回给定的从0开始索引的元素。使用超出[0,foo_size())范围的索引来调用该方法会受到未定义的行为。
  • void set_foo(int index, const Bar value):为给定的从0开始索引的元素赋值。
  • void add_foo(const Bar value):使用给定的值为字段追加一个新元素。
  • void clear_foo():移除字段的所有元素。调用之后,foo_size()将返回0。
  • const RepeatedField<int>& foo() const:返回存储字段元素的基础RepeatedField。这个容器类提供了类似于STL的迭代器和其他方法。
  • RepeatedField<int>* mutable_foo():返回存储字段元素的基础RepeatedField的指针。这个容器类提供了类似于STL的迭代器和其他方法。

重复的内嵌消息字段

给出消息定义:

message Bar { }

定义如下:

repeated Bar foo = 1;

编译器会生成如下方法:

  • int foo_size() const:返回字段中当前元素的数量。
  • const Bar& foo(int index) const:返回给定的从0开始索引的元素。使用超出[0,foo_size())范围的索引来调用该方法会受到未定义的行为。
  • Bar* mutable_foo(int index):返回给定的从0开始索引的元素所存储的可变Bar对象的指针。使用超出[0,foo_size())范围的索引来调用该方法会受到未定义的行为。
  • Bar* add_foo():新增一个空元素并返回它的指针。返回的Bar是可变的,且它的字段全都未设置(即,新分配的Bar对象)。
  • void clear_foo():移除字段的所有元素。调用之后,foo_size()将返回0。
  • const RepeatedField<Bar>& foo() const:返回存储字段元素的基础RepeatedField。这个容器类提供了类似于STL的迭代器和其他方法。
  • RepeatedField<Bar>* mutable_foo():返回存储字段元素的基础RepeatedField的指针。这个容器类提供了类似于STL的迭代器和其他方法。

Oneof数值字段

oneof字段定义如下:

oneof oneof_name {
  int32 foo = 1;
}

编译器会生成如下方法:

  • int32 foo() const:如果oneof case未kFoo,则返回字段当前的值,否则返回默认值。
  • void set_foo(int32 value)
    • 如果同一oneof字段的其他任一oneof已设置,则调用clear_oneof_name()
    • 设置字段的值,并设置oneof case为kFoo
  • void clear_foo()
    • 如果oneof case不为kFoo,则不做任何操作。
    • 如果oneof case为kFoo,清理字段的值及oneof case。

对于其他数值字段类型(包括bool),根据标量值类型表,int32被相应的c++类型替换。

Oneof字符串字段

对于下面任意一个oneof字段定义:

oneof oneof_name {
    string foo = 1;
    
}
oneof oneof_name {
    bytes foo = 1;
    .
}

编译器会生成如下方法:

  • const string& foo() const:如果oneof case未kFoo,则返回字段当前的值,否则返回默认值。
  • void set_foo(const string& value)
    • 如果同一oneof字段的其他任一oneof已设置,则调用clear_oneof_name()
    • 设置字段的值,并设置oneof case为kFoo
  • void set_foo(const char* value)
    • 如果同一oneof字段的其他任一oneof已设置,则调用clear_oneof_name()
    • 使用C风格的空终止符字符串来设置字段的值,并设置oneof case为kFoo
  • void set_foo(const char* value, int size):如上,但使用的给定的大小而不是寻找空终止符。
  • string* mutable_foo()
    • 如果同一oneof字段的其他任一oneof已设置,则调用clear_oneof_name()
    • 设置oneof case为kFoo,并返回存储字段值的可变string对象的指针。如果调用之前oneof case没有设置为kFoo,将会返回空字符串(而不是默认值)。
  • void clear_foo()
    • 如果oneof case不为kFoo,则不做任何操作。
    • 如果oneof case为kFoo,清理字段的值及oneof case。
  • void set_allocated_foo(string* value)
    • 调用clear_oneof_name()
    • 如果字符串指针非空:将字符串对象设置给字段并设置oneof case为kFoo。该消息取得已分配字符串对象的所有权。
  • string* release_foo()
    • 如果oneof case不为kFoo,则返回NULL
    • 清理oneof case,释放该字段的所有权并返回该字符串对象的指针。调用之后,调用者获得已分配字符串对象的所以权。

Oneof枚举字段

给定枚举类型:

enum Bar {
  BAR_VALUE = 0;
  OTHER_VALUE = 1;
}

oneof字段定义如下:

oneof oneof_name {
    Bar foo = 1;
    ...
}

编译器会生成如下方法:

  • Bar foo() const:如果oneof case未kFoo,则返回字段当前的值,否则返回默认值。
  • void set_foo(Bar value)
    • 如果同一oneof字段的其他任一oneof已设置,则调用clear_oneof_name()
    • 设置字段的值,并设置oneof case为kFoo
    • 在debug模式下(即NDEBUG未定义),如果valueBar中所有的值定义都不匹配,该方法会终端进程。
  • void clear_foo()
    • 如果oneof case不为kFoo,则不做任何操作。
    • 如果oneof case为kFoo,清理字段的值及oneof case。

Oneof内嵌消息字段

给定消息类型:

message Bar { }

oneof字段定义如下:

oneof oneof_name {
    Bar foo = 1;
    ...
}

编译器会生成如下方法:

  • bool has_foo() const:如果oneof case未kFoo,则返回true
  • const Bar& foo() const:如果oneof case未kFoo,则返回字段当前的值,否则返回Bar::default_instance()
  • Bar* mutable_foo()
    • 如果同一oneof字段的其他任一oneof已设置,则调用clear_oneof_name()
    • 设置oneof case为kFoo,且返回存储字段值的可变Bar对象的指针。如果调用之前oneof case没有设置为kFoo,则返回所有字段均未设置的Bar(即新分配的Bar)。
    • 调用之后,has_foo()会返回truefoo()会返回一个相同的Bar实例的引用且oneof_name_case()会返回kFoo
  • void clear_foo()
    • 如果oneof case不为kFoo,则不做任何操作。
    • 如果oneof case为kFoo,清理字段的值及oneof case。
  • void set_allocated_foo(Bar* value)
    • 调用clear_oneof_name()
    • 如果Bar指针非空:将Bar对象设置给字段并设置oneof case为kFoo。该消息取得已分配字符串对象的所有权,has_foo()会返回true,且oneof_name_case()会返回kFoo
    • 如果Bar指针为空,则has_foo()会返回false,且oneof_name_case()会返回ONEOF_NAME_NOT_SET。(与调用clear_oneof_name()行为类似)
  • Bar* release_foo()
    • 如果oneof case不为kFoo,则返回NULL
    • 如果oneof case为kFoo,清理oneof case,释放该字段的所有权并返回该Bar对象的指针。调用之后,调用者获得已分配Bar对象的所以权。has_foo()会返回falsefoo()会返回默认值且oneof_name_case()会返回ONEOF_NAME_NOT_SET

映射字段

映射字段定义如下:

map<int32, int32> weight = 1;

编译器会生成下列访问器方法:

  • const google::protobuf::Map<int32, int32>& weight();:返回一个不可变的Map
  • google::protobuf::Map<int32, int32>* weight();:返回一个可变的Map

在Protocol Buffer中,google::protobuf::Map是用来存储映射字段的特定容器。从下面的接口可以看出,它使用std::mapstd::unordered_map的常用方法的子集。

template<typename Key, typename T> {
class Map {
  // Member types
  typedef Key key_type;
  typedef T mapped_type;
  typedef MapPair< Key, T > value_type;

  // Iterators
  iterator begin();
  const_iterator begin() const;
  const_iterator cbegin() const;
  iterator end();
  const_iterator end() const;
  const_iterator cend() const;
  // Capacity
  int size() const;
  bool empty() const;

  // Element access
  T& operator[](const Key& key);
  const T& at(const Key& key) const;
  T& at(const Key& key);

  // Lookup
  int count(const Key& key) const;
  const_iterator find(const Key& key) const;
  iterator find(const Key& key);

  // Modifiers
  pair<iterator, bool> insert(const value_type& value);
  template<class InputIt>
  void insert(InputIt first, InputIt last);
  size_type erase(const Key& Key);
  iterator erase(const_iterator pos);
  iterator erase(const_iterator first, const_iterator last);
  void clear();

  // Copy
  Map(const Map& other);
  Map& operator=(const Map& other);
}

新增数据的最简单的方法就是使用常用的map语法,例如:

std::unique_ptr<ProtoName> my_enclosing_proto(new ProtoName);
(*my_enclosing_proto->mutable_weight())[my_key] = my_value;

pair<iterator, bool> insert(const value_type& value)会隐式调用value_type实例的深拷贝。如下,是向google::protobuf::Map插入新值最高效的方法:

T& operator[](const Key& key): map[new_key] = new_mapped;
在标准map中使用google::protobuf::Map

google::protobuf::Map支持与std::mapstd::unordered_map一样的迭代器。如果你不想直接使用google::protobuf::Map,你可以使用如下操作将google::protobuf::Map转化为标准的map:

std::map<int32, int32> standard_map(message.weight().begin(),
                                    message.weight().end());

注意,这将生成为整个映射生成一个深拷贝。

你也可以用下面的方式将标准的map结构化为google::protobuf::Map

google::protobuf::Map<int32, int32> weight(standard_map.begin(), standard_map.end());
解析未知变量

在网络上,.proto映射相当于每个键值对的映射条目消息,而映射本身是映射条目的重复字段。就像普通的消息类型,解析过的映射条目消息中可能有未知字段:在映射中,int64字段被定义为map<int32, string>

在网络格式中,如果一个映射条目消息中有未知字段,未知字段将会被丢弃。

如果一个映射条目消息中有一个未知的枚举变量,proto2和proto3有着不同的处理方式。在proto2中,整个映射条目消息将被放入包含消息的未知字段集中。在proto3中,未知的枚举变量会像已知的一样被放入映射字段中。

Any

给出如下的Any定义:

import "google/protobuf/any.proto";

message ErrorStatus {
  string message = 1;
  google.protobuf.Any details = 2;
}

在生成的代码中,获取字段的detials的getter方法返回一个google::protobuf::Any的实例,它提供如下的用于打包和解包Any变量的特定方法:

class Any {
 public:
  // Packs the given message into this Any using the default type URL
  // prefix “type.googleapis.com”.
  void PackFrom(const google::protobuf::Message& message);

  // Packs the given message into this Any using the given type URL
  // prefix.
  void PackFrom(const google::protobuf::Message& message,
                const string& type_url_prefix);

  // Unpacks this Any to a Message. Returns false if this Any
  // represents a different protobuf type or parsing fails.
  bool UnpackTo(google::protobuf::Message* message) const;

  // Returns true if this Any represents the given protobuf type.
  template<typename T> bool Is() const;
}

Oneof

给出如下的oneof定义:

oneof oneof_name {
    int32 foo_int = 4;
    string foo_string = 9;
    ...
}

编译器将生成如下的C++枚举类型:

enum OneofNameCase {
  kFooInt = 4,
  kFooString = 9,
  ONEOF_NAME_NOT_SET = 0
}

此外,还会生成这些方法:

  • OneofNameCase oneof_name_case() const:如果字段被设置了,则返回对于的枚举值;否则,返回ONEOF_NAME_NOT_SET
  • void clear_oneof_name():如果oneof字段使用指针设置(消息或字符串),则释放该指针,且将oneof case设置为ONEOF_NAME_NOT_SET

枚举

给出如下的枚举定义:

enum Foo {
  VALUE_A = 0;
  VALUE_B = 5;
  VALUE_C = 1234;
}

编译器会生成名为Foo的C++枚举类型,其值与设置的一样。此外,还会生成下面的函数:

  • const EnumDescriptor* Foo_descriptor():返回该类型的描述,包括该枚举类型定义的变量的信息。
  • bool Foo_IsValid(int value):如果给定的数字与Foo中定义的值匹配则返回true
  • const string& Foo_Name(int value):返回给定数字的名称。如果该值不存在则返回空字符串。如果多个值使用这个数字,则返回定义的第一个。在上面的例子中,Foo_Name(5)返回VALUE_B
  • bool Foo_Parse(const string& name, Foo* value):如果name在该枚举中可用,则将值赋值给value并返回true。在上面的例子中,Foo_Parse("VALUE_C", &some_foo)会返回true,且设置some_foo为1234。
  • const Foo Foo_MIN:该枚举类型中的最小可用值(示例中为VALUE_A)。
  • const Foo Foo_MAX:该枚举类型中的最大可用值(示例中为VALUE_C)。
  • const Foo Foo_ARRAYSIZE:总是被定义为Foo_MAX+1

你可以在消息类型中定义一个枚举。这种情况下,编译器生成的代码是将它声明为消息类的内嵌枚举类型。Foo_descriptor()Foo_IsValid()会被声明为静态函数。实际上,枚举类型本身和它的值使用重组后的名称被声明为全局范围可用,使用typedef和一些常量定义的方式导入类的范围。这样做只是为了避免声明排序的问题。假如枚举真的被内嵌到消息类型中,不要依赖重组后的头部名称。

扩展(仅proto2)

给出带有扩展范围的消息类型:

message Foo {
  extensions 100 to 199;
}

编译器会为Foo生产一些额外的方法:HasExtension()ExtensionSize()ClearExtension()GetExtension()SetExtension()MutableExtension()AddExtension()SetAllocatedExtension()ReleaseExtension()。每个方法的第一个参数是一个扩展标识符(如下所述),它标识一个扩展字段。其余的参数和返回值与对应的访问方法的参数和返回值完全相同,这些访问方法将为与扩展标识符类型相同的普通(非扩展)字段生成。(GetExtension()对应于没有特殊前缀的访问器。)

给出如下的扩展定义:

extend Foo {
  optional int32 bar = 123;
  repeated int32 repeated_bar = 124;
}

对于单个的扩展字段bar,编译器生成一个名为bar的“扩展标识符”,你可以使用Foo的访问器来访问该扩展,如下:

Foo foo;
assert(!foo.HasExtension(bar));
foo.SetExtension(bar, 1);
assert(foo.HasExtension(bar));
assert(foo.GetExtension(bar) == 1);
foo.ClearExtension(bar);
assert(!foo.HasExtension(bar));

类似地,对于重复字段repeated_bar,编译器生成一个名为repeated_bar的“扩展标识符”,你可以使用Foo的访问器来访问它:

Foo foo;
for (int i = 0; i < kSize; ++i) {
  foo.AddExtension(repeated_bar, i)
}
assert(foo.ExtensionSize(repeated_bar) == kSize)
for (int i = 0; i < kSize; ++i) {
  assert(foo.GetExtension(repeated_bar, i) == i)
}

(扩展标识符的确切实现是复杂的,涉及到模板的神奇使用——但是,你不需要担心扩展标识符是如何使用它们的。)

扩展可以声明为其它类型的内嵌类型。例如,常见的模式如下:

message Baz {
  extend Foo {
    optional Baz foo_ext = 124;
  }
}

这种情况下,扩展标识符foo_ext被声明为Baz的内嵌类型。使用方法如下:

Foo foo;
Baz* baz = foo.MutableExtension(Baz::foo_ext);
FillInMyBaz(baz);

Arena分配

Arena分配是仅C++有的功能,在使用Protocol Buffer时,它可以帮助你优化你的内存使用,提高性能。在.proto文件中启用Arena分配会在生成的C++代码中添加处理Arena分配的额外代码。关于Arena分配API的细节,详见Arena Allocation Guide

服务

如果.proto文件中包含下面的内容:

option cc_generic_services = true;

之后,Protocol Buffer编译器会根据在本节中描述的文件中找到的服务定义生成代码。然而,生成的代码可能不受欢迎,因为它并没有绑定特定的RPC系统,因此需要为一个系统定制更多级别的间接代码。如果你不想生成这样的代码,你可以在文件中添加这一行:

option cc_generic_services = false;

如果上面的行都未给定,默认选项未false,因此不推荐使用通用服务。(注意,2.4.0版本之前,默认选项为true。)

基于.proto语言服务定义的RPC系统应该提供插件来生成适合系统的代码。这些插件可能需要禁用抽象服务,以便它们可以生成自己的同名类。插件是2.3.0版本(2010年1月)新引入的。

下面的章节描述抽象服务启用时编译器会生成什么。

接口

给出如下服务定义:

service Foo {
  rpc Bar(FooRequest) returns(FooResponse);
}

编译器会生成一个名为Foo的类来表示该服务。Foo中包含服务定义中定义的每个方法的虚方法。这种情况下,Bar方法定义如下:

virtual void Bar(RpcController* controller, const FooRequest* request,
                 FooResponse* response, Closure* done);

这些参数等同于Service::CallMethod()的参数,只是method参数是隐含的,而requestresponse指定了它们的确切类型。

这些生成的方法是虚,但不是纯虚的。默认的实现知识简单地调用controller->SetFailed(),并使用一条错误消息指示方法未实现,然后调用done回调。在实现你自己的服务时,你必须子类化这个生成的服务并适当的实现它的方法。

Foo子类化Service接口。编译器会自动实现如下的Service的方法:

  • GetDescriptor:返回服务的ServiceDescriptor
  • CallMethod:根据提供的方法描述符确定要调用哪个方法,并直接调用它,将请求和响应消息对象向下转换为正确的类型。
  • GetRequestPrototypeGetResponsePrototype:针对给定的方法,返回当前类型的请求/响应的默认实例。

也会生成下列静态方法:

  • static ServiceDescriptor descriptor():返回类型的描述,其中包括该服务包含那些方法以及它们的输入输出类型。

Stub

编译器还为每个服务接口生成了一个“Stub”实现,由要向实现服务的服务器发送请求的客户端使用。对于Foo服务,Stub实现被定义未Foo_Stub。就像内嵌消息类型一样,由于typedef的使用,所以可以使用Foo::Stub来引用Foo_Stub

Foo_Stub是实现了如下方法的Foo的子类:

  • Foo_Stub(RpcChannel* channel):在给定的通道上发送请求的新构造的Stub。
  • Foo_Stub(RpcChannel* channel, ChannelOwnership ownership):在给定的通道上发送请求的新构造的Stub,及通道的所有者。如果ownershipService::STUB_OWNS_CHANNEL,之后在删除stub对象时也会删除该通道。
  • RpcChannel* channel):返回传递给构造函数的stub通道。

stub还额外实现了作为通道打包器的每个服务方法。调用其中一个方法只是简单地调用channel->CallMethod()

Protocol Buffer库并不包含RPC实现。但是,它包括将生成的服务类连接到你所选择的任意RPC实现所需的所有工具。你只需要提供RpcChannelRpcController的实现。更多信息,详见service.h

插件

想要扩展c++代码生成器输出的代码,Code generator plugin可以使用给定的插入点名称插入下列类型的代码。除非另做说明,否则每个插入点都出现在.pb.cc.pb.h文件中。

  • includes:include命令。
  • namespace_scope:属于文件包/名称空间,但不属于任何特定类的声明。出现在所有其他名称空间范围代码之后。
  • global_scope:属于顶级声明,位于文件级明明空间之外。出现在文件末尾。
  • class_scope:TYPENAME:属于消息类的成员声明。TYPENAME是完整的proto名称,例如package.MessageType。在类中,出现在其它公共声明之后。只出现在.pb.h文件中。

Arena分配指南

为什么要用Arena分配
开始
Arena类API
生成消息类
使用模式和最佳实践
示例

Arena分配是仅C++有的功能,在使用Protocol Buffer时,它可以帮助你优化你的内存使用,提高性能。本章节是上一章节的补充,它实际上描述了再arena分配启用时,编译器生成了什么。本文默认你已经熟悉了语言指南C++代码生成指南

为什么要用Arena分配

在Protocol Buffer代码中,内存的分配与释放占据了CPU耗时的很大一部分。默认情况下,Protocol Buffer为每个消息对象、它的每个子对象,以及一些字段类型,比如字符串,在堆上进行内存分配。在解析消息和构建新的消息时,这个分配操作会大量发生;当消息及其子对象树释放时,会产生相关应的释放操作。

基于Arena的分配被设计用来减小这一性能开销。使用arena分配,新对象从一个叫做arena的大内存块中分配。通过丢弃整个arena,可以一次释放所有对象,理想情况下不需要运行任何被包含对象的析构函数(虽然arena仍然可以在需要时维护一个析构函数列表)。通过将对象分配减少为一个简单的指针增量,这使得对象分配变得更快,而且使释放时几乎没有消耗。Arena分配还提供了更高的缓存效率:当消息被解析时,它们更有可能被分配到连续内存中,这使得遍历消息更有可能到达热缓存线路。

为了获得这些好处,你需要了解对象的生命周期,并选择合适的粒度来使用arena(对于服务器,这通常是针对每个请求的)。在使用模式和最佳实践中,你可以了解更多。

开始

首先你需要在每个.proto文件中启用arena分配。那么你需要在你的.proto文件中添加如下option

option cc_enable_arenas = true;

这就告诉编译器为你的消息使用arena分配生成额外的代码,使用如下:

#include <google/protobuf/arena.h>
{
  google::protobuf::Arena arena;
  MyMessage* message = google::protobuf::Arena::CreateMessage<MyMessage>(&arena);
  // ...
}

只要arena存在,通过CreateMessage()创建的消息对象就一直存在,而且你不应该delete返回的消息指针。该消息的所有内部存储(少数例外$^1$)以及子类消息(例如,MyMessage中重复字段的子类消息)也都在arena上分配。

在大多数情况下,代码的其余部分与不使用arena分配是一样的。

在下一小节中会看到更多的arena API的细节,在文末的示例中你能看到更多使用细节。

1 目前,即使包含的消息在arena中,字符串字段也将其数据存储在堆中。未知字段也是在堆上分配。

Arena类API

在arena上,你可以使用google::protobuf::Arena类来创建消息对象。该类实现了下列的公共方法。

构造函数

  • Arena():使用默认参数创建一个新的arena,针对一般的使用场景。
  • Arena(const ArenaOptions& options):使用特定的分配选项来创建一个新的arena。在ArenaOptions中,可用的选项能够使用一个初始的用户提供的内存块分配之前采取系统分配程序,控制初始和最大请求大小的内存块,并允许你通过自定义块分配和回收函数指针来构建释放列表和其它顶上的块。

分配方法

  • template<typename T> static T* CreateMessage(Arena* arena):在arena上创建一个新的消息类型为T的Protocol Buffer对象。消息类型必须是定义在.proto文件中,且文件中有option cc_enable_arenas = true;,否则,将导致编译错误。 如果arena非空,将返回在arena上的消息对象,它的内部存储以及子类消息(如果有的化)也都在同一arena上分配,且它的声明周期与该arena的一样。该对象不能手动释放:该arena拥有消息对象的生命周期。
    如果arena为空,返回的消息对象被分配在堆上,调用者拥有该对象的所有权。
  • template<typename T> static T* Create(Arena* arena, args...):与CreateMessage()类似,但允许你在arena上创建任何类的对象,即使Protocol Buffer消息类型没有option cc_enable_arenas = true;:你可以从不支持的arena的文件中使用Protocol Buffer消息类,或任意的C++类。例如,你有如下的C++类:
class MyCustomClass {
    MyCustomClass(int arg1, int arg2);
    // ...
};

你可以在arena上创建它的实例:

void func() {
    // ...
    google::protobuf::Arena arena;
    MyCustomClass* c = google::protobuf::Arena::Create<MyCustomClass>(&arena, constructor_arg1, constructor_arg2);
    // ...
}
  • template<typename T> static T* CreateArray(Arena* arena, size_t n):如果arena非空, 该方法将为n个类型为T的元素分配原始存储并返回它。arena所有这返回的内存并在自身销毁时释放它。如果arena为空,该方法在堆上分配存储且调用者获得所有权。

T必须有一个简单的构造函数:当数组在arena上创建时,构造函数并不会被调用。

“所有权列表”方法

下面的方法允许你指定特定对象或析构函数为arena所有,从而确保在arena删除它自己的时候也删除它们:

  • template<typename T> void Own(T* object):添加object到arena所有的堆对象列表中。当arena销毁时,它将遍历整个列表并使用删除操作释放每个对象,即系统内存分配。当一个对象的声明周期要跟arena绑定,但它本身又不是在arena上分配时,这种情况下该方法很有用。
  • template<typename T> void OwnDestructor(T* object):将object的析构函数添加到arena的析构函数调用列表中。当arena销毁时,它将遍历整个列表并将调用每个析构函数。它不会试图释放对象的底层内存。当一个对象是内嵌在arena分配的存储中但它的析构函数并不会被调用的情况下,该方法是有用的,例如,因为它的包含类是一个析构函数不会被调用的protobuf消息,或者是因为它是通过AllocateArray()手动在被分配的块上构造的。

其它方法

  • uint64 SpaceUsed() const:返回arena的总大小,它是所有底层块大小的总和。该方法时线程安全的;但是如果是多线程并发分配,该方法的返回值可能不包括那些新块的大小。
  • uint64 Reset():销毁arena存储:首先调用所有注册的析构函数且释放所有注册的堆对象,之后丢弃所有的arena块。这个销毁过程与arena的析构函数运行时发生的过程是等价的,只是arena在这个方法返回后会被重用。返回arena使用的总大小:此信息对于调优性能非常有用。
  • template<typename T> Arena* GetArena():返回arena的指针。虽然不是很有用,但它允许在需要GetArena()方法的模板实例化中使用Arena()

线程安全

google::protobuf::Arena的分配方法是线程安全的,且底层实现有一定的长度来确保多线程分配更快。Reset()方法不是线程安全的:执行arena重置的线程必须首先与所有执行分配或者使用arena中分配的对象的线程同步。

生成消息类

当你启用arena分配时,下面的消息类成员会被改变或添加。

消息类方法

  • Message(Message&& other):如果源消息不在arena上,move构造高效地将一个消息的所有字段移动到另一个消息,而无需进行赋值或堆分配(该操作的时间复杂度为0(number-of-declared-fields))。但是如果源消息在arena上,它将执行底层数据的深拷贝操作。以上两种情况中,源消息还是有效的但未指定的状态。
  • Message& operator=(Message&& other):无论两个消息都不在arena或是同一个arena,赋值操作高效地将一个消息的所有字段移动到另一个消息,而无需进行赋值或堆分配(该操作的时间复杂度为0(number-of-declared-fields))。如果只要一个消息在arena上或不在同一个arena上,它将执行底层数据的深拷贝操作。以上两种情况中,源消息还是有效的但未指定的状态。
  • void Swap(Message* other):如果要交换的两个消息都不在arena或在同一个arena上,Swao()的行为与未启用arena分配相同:它将高效地交换消息对象的内容,通常是通过廉价的指针交换以及尽可能地避免拷贝。如果只用一个消息在arena上或两个消息不在同一个arena上,Swap()将执行底层数据的深拷贝操作。这一操作是很有必要的,因为交换之后的子对象可能有不同的生命周期,引起use-after-free错误。
  • Message* New(Arena* arena):对标准New()方法的替换重写。它允许该类型在给定的arena上创建新的消息对象。他的语义与Arena::CreateMessage<T>(arena)相同,前提是它所调用的具体消息类型是在启用Arena分配的情况下生成的。如果消息类型不是在启用Arena分配的情况下生成的,当arena非空时,它等同于一个后跟arena->Own(message)的原始分配。
  • Arena* GetArena():返回分配此消息对象的arena(如果有的话)。
  • void UnsafeArenaSwap(Message* other):与Swap()相同,只是它假设两个对象在同一个arena上(或两个都不在arena上),并且总是使用这个操作的高效指针交换实现。使用该方法可以提升效率,因为,不像Swap(),在执行交换之前,它不需要检查那个消息位于那个arena上。正如Unsafe前缀所说的,只有在确定消息不在不同的arena上时,你才能使用该方法;否则,该方法可能产生不可预测的结果。

内嵌消息字段

当你在arena上分配消息对象时,它的内嵌消息字段对象(子消息)自动为该arena所有。如何分配消息对象取决于它们在哪定义的:

  • 如果消息类型是在启用Arena分配的.proto文件中定义的,则对象就直接在arena上分配。
  • 如果消息类型是在另一个没有启用Arena分配的.proto文件中定义的,该对象是在堆上分配的,但它的所有权归父消息的arena所有。这意味着在arena销毁时,属于该arena的对象也会被释放。

下列字段定义之一:

optional Bar foo = 1; required Bar foo = 1;

在启用arena分配情况下,下面的方法会被添加或有一些特殊的行为。否则,访问器方法只是用默认行为

  • Bar* mutable_foo():返回子消息实例的可变指针。如果父对象在arena上,返回的对象也在arena上。
  • void set_allocated_foo(Bar* bar):接受一个新对象并将其作为字段的新值。Arena支持新增了额外的复制语义,来确保对象在跨越arena/arena或arena/heap边界时保持适当的所有权:
    • 如果父对象在堆上且bar也在堆上,或父对象和子消息在同一arena上,该消息的行为不变。
    • 如果父对象在arena上且bar在堆上,父消息使用arena->Own()bar添加到它自己的arena所有去列表。
    • 如果父对象在arena上且bar在另一个arena上,该方法生成消息的副本将将其作为字段的新值对待。
  • Bar* release_foo():如果存在返回字段已存在的子消息实例,如果不存在则返回空指针;将该实例的所有权移交给调用者并清理父消息字段。Arena支持新增了额外的复制语义,以确保返回的对象总是遵守heap-allocated协议:
    • 如果父消息在arena上,该方法在堆上创建子消息的副本,清空该字段的值,并返回该副本。
    • 如果父消息在堆上,该消息行为不变。
  • void unsafe_arena_set_allocated_foo(Bar* bar):与set_allocated_foo相同,但假定父消息和子消息都在同一个arena。使用该方法可以提升性能,因为它不需要检查消息是不是在特定的arena或堆上。只有在确定父消息在arena上且子消息也在同一arena上(或声明周期与该arena相同)才能使用该方法。
  • Bar* unsafe_arena_release_foo():与release_foo()类似,但假定父消息在arena上,且返回一个不应该被直接删除的arena-allocated对象。只有当父消息在arena上时才能使用该方法。

字符串字段

目前,即使父消息在arena上,字符串字段也将它们的数据存储在堆上。因此,即使arena分配启用,字符串访问器方法使用默认行为

当arena启用,字符串和字节字段生成unsafe_arena_release_field()unsafe_arena_set)allocated_field()方法。注意这些方法已被弃用,且之后会被删除。这些方法是被错误地添加的,与它们的安全方法相比并没有性能优势。

重复字段

当包含的消息是在arena上分配的,重复字段也在arena上分配它们的内部数组存储;当这些元素是由指针(消息或字符串)保留的独立对象时,也在arena上分配它们的元素。在消息类级别,为重复字段生成的方法不变。在arena支持启用时,由访问器返回的RepeatedFieldRepeatedPtrField对象确实有新的方法和语义的改变。

重复数值字段

在arena支持启用时,包含原始类型RepeatedField对象有下列新/变化的方法:

  • void UnsafeArenaSwap(RepeatedField* other):在无需验证该重复字段和另一个是不是在同一个arena的情况下执行RepeatedField内容的交换。如果它们不在同一个arena上,那这两个重复字段对象必须在生命周期相同的arena上。一个在arena上另一个在堆上的情况下,先检查然后禁用。
  • void Swap(RepeatedField* other):检查每个重复字段对象的arena,如果一个在arena另一个在堆上或两个都在不同的arena上,在交换之前复制底层数组。这意味着交换结束后,每个重复字段都在它自己的arena或堆上保留一个合适的数组。
重复的内嵌消息字段

在arena支持启用时,包含消息的RepeatedPtrField对象有下列新/变化的方法:

  • void UnsafeArenaSwap(RepeatedPtrField* other)::在无需验证该重复字段和另一个是不是在同一个arena指针的情况下执行RepeatedField内容的交换。如果它们不在同一个arena指针上,那这两个重复字段对象必须在生命周期相同的arena指针上。一个对象有一个非空的arena上指针而另一个有一个空的arena指针,这种情况下先检查然后禁用。
  • void Swap(RepeatedPtrField* other):检查每个重复字段对象的arena指针,如果一个非空(包含在arena上),另一个为空(包含在堆上),或两个都为非空但值不同,在交换之前会复制底层数组和指向对象的指针。这意味着交换结束后,每个重复字段都在它自己的arena或堆上保留一个合适的数组。
  • void AddAllocated(SubMessageType* value):检查给定的消息对象与重复字段的arena指针是不是一样。如果实在同一个arena上,之后对象的指针就直接添加到底层数组。否则,会生成一个副本,如果对象实在堆上分配的原始的对象会被释放,副本会被放入底层数组。这保证重复字段所指向的所有对象的指针与重复字段的arena指针指向的所有权域(堆或这指定的arena)相同。
  • SubMessageType* ReleaseLast():返回与重复字段最后一个消息相同的堆上分配的消息,并从重复字段中移除它。如果重复字段本身有一个空的arena指针(即它所有指向消息的指针都时堆分配的),之后该方法只是简单的返回原始对象的指针。如果重复字段有一个非空的arena指针,该方法会在堆上分配一个副本然后返回该副本。上述两种情况下,调用者会的堆上分配的对象的所有权,并负责删除该对象。
  • void UnsafeArenaAddAllocated(SubMessageType* value):与AddAllocated()类似,但是不处理堆/arena检查或任何消息的副本。它直接将提供的指针添加到该重复字段的内部数组指针中。如果重复字段有一个空的arena指针,调用者必须保证提供的对象是在堆上分配的,或者如果重复字段有一个非空的arena指针,则必须在arena分配(同一个arena或相同生命周期的arena)的。
  • SubMessageType* UnsafeArenaReleaseLast():与ReleaseLast()类似,但不处理任何副本,即使重复字段有一个非空的arena指针。相反,它直接返回该对象在重复字段中的指针。如果重复字段的arena指针为空,返回的对象是在堆上的,如果重复字段的arena指针非空,则是在arena上。如果对象是堆分配的,调用者获得所有权;如果对象是arena分配的,调用者不能删除返回的对象。
  • void ExtractSubrange(int start, int num, SubMessageType** elements):从索引start位置开始,从重复字段中提取num个元素,如果elements非空的话将移除的元素放入elememts中。如果重复字段在arena上,在元素返回之前先将这些元素复制到堆上。这两中情况下(在或不在arena上),调用者拥有堆上的返回对象。
  • void UnsafeArenaExtractSubrange(int start, int num, SubMessageType** elements):从索引start位置开始,从重复字段中提取num个元素,如果elements非空的话将移除的元素放入elememts中。与ExtractSubrange()不同,该方法从不复制提取的元素。
重复的字符串字段

重复的字符串字段有与重复的消息字段一样的新方法和修改的语义,因为它们都是通过指针引用来保存它们的底层数据(即字符串)。

使用模式和最佳实践

在使用arena分配的消息时,有几种使用模式可能导致意外的副本或其它负面性能的影响。你应该留意,下面的几种常用的模式在适配arena的代码时需要要有所改变。(注意,在API设计中我们已经注意到这一点,以确保依然正确的行为 — 但是更高性能的解决方案仍可能需要一些返工。)

意外的副本

在启用arena时,有些在未启用arena时并不会创建对象副本的方法可能会创建副本。如果你确保分配的对象合适/或使用提供的arena特定版本的方法,就可以避免这些不需要的副本,下面将对此进行更详细的描述。

设置分配/添加分配/释放

默认情况下,release_field()set_allocate_field()方法(针对单个消息字段)以及ReleaseLast()AddAllocated()方法(针对重复消息字段)允许用户代码直接添加和分离子消息,通过指针的所有权而无需复制任何数据。

然尔,当父消息在arena上时,这些方法有时候需要复制传入/返回的对象,以保持与现有的所有权兼容。更具体的,当父消息在arena上而新的子消息不在,获取所有权的方法(set_allocated_field()AddAllocated())可能会复制数据;反之亦然;或它们不在同一arena上。如果父消息在arena上,释放所有权的方法(release_field()ReleaseLast())可能会复制数据,因为按照约定返回的对象必须是在堆上。

为了避免这些复制,我们添加了这些方法的unsafe arena版本的协议,在这些版本中复制绝不会被执行:对单个和重复字段,分别是unsafe_arena_set_allocated_field()unsafe_arena_release_field()UnsafeArenaAddAllocated()UnsafeArenaRelease()。只有在你了解这么做是安全且父对象和子对象都能如期分配是,你才能使用这些方法。否则,比如,你可能获得具有不同生命周期的父对象和子对象,这将导致use-after-free错误。

下面是你如何使用这些方法来避免不必要的复制的例子。接下来会在arena上创建下面的消息:

Arena* arena = new google::protobuf::Arena();
MyFeatureMessage* arena_message_1 =
  google::protobuf::Arena::CreateMessage<MyFeatureMessage>(arena);
arena_message_1->mutable_nested_message()->set_feature_id(11);

MyFeatureMessage* arena_message_2 =
  google::protobuf::Arena::CreateMessage<MyFeatureMessage>(arena);

下面的代码是release_...()API的低效用法:

arena_message_2->set_allocated_nested_message(arena_message_1->release_nested_message());

arena_message_1->release_message(); // returns a copy of the underlying nested_message and deletes underlying pointer

使用unsafe arena版可以避免复制:

arena_message_2->set_allocated_nested_message(
   arena_message_1->unsafe_arena_release_nested_message());

关于这些方法的更多细节,你可以在上面的内嵌消息字段章节了解到。

交换

如果两个消息处在不同的arenas上,或一个在arena另一个在堆上,使用Swap()交换两个消息的内容时,底层的子对象可能会被复制。如果你想避免这个复制,且知道这两个消息在同一个arena上或在有相同生命周期的不同arena上,或知道这两个消息都在堆上,你可以使用新的方法 — UnsafeArenaSwap()。此方法既避免了执行arena检查的开销,又避免了在可能发生副本检查的情况下进行副本检查。

例如,下面的代码在Swap()调用中会引起复制:

MyFeatureMessage* message_1 =
  google::protobuf::Arena::CreateMessage<MyFeatureMessage>(arena);
message_1->mutable_nested_message()->set_feature_id(11);

MyFeatureMessage* message_2 = new MyFeatureMessage;
message_2->mutable_nested_message()->set_feature_id(22);

message_1->Swap(message_2); // Inefficient swap!

在上述代码中要避免复制,你可以在相同的arena像message_1一样分配message_2

MyFeatureMessage* message_2 =
   google::protobuf::Arena::CreateMessage<MyFeatureMessage>(arena);
内嵌消息字段和arena启用选项

每个.proto都有它自己的arena支持的“功能开关”。如果给定的.proto文件中并没有设置cc_enable_arenas,那么文件中的类型定义并不会存储到arena上,即使其它类型中包含在了该文件中定义的子消息类型中。换言之,cc_enable_arenas是不可传递的。相反,具有arena资格但本身并不支持arena的消息的子消息将始终存储在堆上,且会被添加到父消息的arena的Own()中以便将它们的生命周期与arena的绑定。

这样约定的原因是,如果arena因为一些额外的代码而未被使用,那么添加arena支持会增加一些开销。所以我们选择(目前)不启用arena的全局支持。而且,由于类型和API兼容性的原因,每个proto消息类型只能有一个C++生成的类,因此我们不能同时生成带arena支持和不带arena支持的类版本。未来,功能优化之后,我们可能会取消这个限制,全局启用arena支持。不过现在,应该为尽可能多的子消息启用它以提高性能。

粒度

我们发现在大多数服务使用场景中,“arena-per-request”模式表现良好。你可能会尝试进一步扩大arena使用,以减少堆开销(通过更频繁的销毁较小的arena),或者减少感知到的线程竞争问题。然而就如我们上面所说的,使用更细粒度的arena可能会导致意外的消息复制。我们还为多线程用例优化了Arena实现,因此单个的arena应该适合在整个请求的生命周期中使用,即使是多线程处理该请求。

示例

下面是一个简单的完整示例,演示了arena分配API的一些特性。

// my_feature.proto

syntax = "proto2";
import "nested_message.proto";

package feature_package;

option cc_enable_arenas = true;

// NEXT Tag to use: 4
message MyFeatureMessage {
  optional string feature_name = 1;
  repeated int32 feature_data = 2;
  optional NestedMessage nested_message = 3;
};
// nested_message.protofset

syntax = "proto2";

package feature_package;

// add cc_enable_arenas on each submessage for
// the best performance when using arenas.
option cc_enable_arenas = true;

// NEXT Tag to use: 2
message NestedMessage {
  optional int32 feature_id = 1;
};

消息构造与再分配:

#include <google/protobuf/arena.h>

Arena arena;

MyFeatureMessage* arena_message =
   google::protobuf::Arena::CreateMessage<MyFeatureMessage>(&arena);

arena_message->set_feature_name("Proto2 Arena");
arena_message->mutable_feature_data()->Add(2);
arena_message->mutable_feature_data()->Add(4);
arena_message->mutable_nested_message()->set_feature_id(247);

声明:本作品采用署名-非商业性使用-相同方式共享 4.0 国际 (CC BY-NC-SA 4.0)进行许可,使用时请注明出处。
Author: mengbin92
Github: mengbin92
cnblogs: 恋水无意