很多人可能听过或者用过Google的ProtoBuffers,但是它还有一个兄弟叫做FlatBuffers。这兄弟俩十分类似,但是FlatBuffers使用起来更节省内存,性能要好一点。和ProtoBuffers相比,FlatBuffers的灵活性要差一点。FlatBuffers是是Google提供的,用于游戏编程的工具箱中的一员。

FlatBuffers主要一个数据序列化工具,它把一个层级的数据结构序列化到一段连续的内存中去,用于在网络间发送、或者在硬盘上保存。FlatBuffers原始支持C++,现在已经拓展了其他许多语言的支持。

IDL和Schema

我们可以用某个编程语言(如C语言)自己的语法和方式来定义所需的数据结构。但这个方式存在一种问题,就是无法保证在多个语言中通用。 如果硬要保证通用性,通常我们会用描述能力比较弱的语言(比如C语言)来定义这个结构,然后仗着其他语言对于C语言的兼容性,来实现在多个语言中使用这个数据结构。这里存在的痛点是,有一些问题是C语言的描述能力无法解决的。

如果我们能使用一种专门用来描述接口的语言,IDL(Interface Description Language)来描述接口的数据结构,然后再通过编译的方式来把转化IDL,并生成目标语言的数据结构,以及处理这些数据结构的操作,那么跨语言之间的兼容性就会大大提升。这种用IDL来描述接口的方式也就是基于Schema的,一个Schema包含若干接口定义,并且可以被IDL编译器转化为目标语言的源代码。

ProtoBuffers是一个例子,它有自己的IDL,接口的定义保存到一个.proto文件中,然后将其转化为C/C++、或者其他语言的代码。与此类似,FlatBuffers的定义是保存到一个.fbs文件,然后通过flatc(参考Using the schema compiler)这个schema转换器来编译生成目标代码。

macOS上可以通过brew install flatbuffers安装FlatBuffers。Vim中的.fbs文件的语法着色可以使用 vim-flatbuffers

Flatbuffers的IDL

下面是摘自Writing a schema的一个例子:

// example IDL file

namespace MyGame;

attribute "priority";

enum Color : byte { Red = 1, Green, Blue }

union Any { Monster, Weapon, Pickup }

struct Vec3 {
  x:float;
  y:float;
  z:float;
}

table Monster {
  pos:Vec3;
  mana:short = 150;
  hp:short = 100;
  name:string;
  friendly:bool = false (deprecated, priority: 1);
  inventory:[ubyte];
  color:Color = Blue;
  test:Any;
}

root_type Monster;

总的来说FlatBuffers的IDL是从C语言的语法中跳脱出来的,它有整形值、字符串、enum、union、struct还有table等等。数据有两种基本的组织方式,标量(Scalar)和非标量(Non-scalar)。标量就是像整形这种包含单个值的类型;而向量(Vector),作为典型的非标量,则是一个数组。

FlatBuffers,以下简称(FBS)中的标量可以带有默认值,比如mana:short = 150。FBS的一个有趣的地方是,带有默认值的参数在序列化的时候是被忽略掉的,不会占据生成的序列的空间。默认值是在生成的代码中处理的。在挑选默认值的时候可不能随便拍脑袋,修改了默认值会破坏向后兼容性,要替换以前的代码才能正常工作。此外,由于这种语义的存在,把一个标量设为默认值和不设置这个标量没有区别。

可以使用flatc的--force-defaults选项来强制在生成的代码中把默认值放到目标序列中。

对于向量(例如inventory:[ubyte];)来说,默认值都是NULL。

struct和table

FBS支持struct和table,前者有点类似C语言中的struct,是实打实的数据结构。struct中的所有字段在序列化的时候会被放置到目标序列中。而table则不太一样,在序列化的时候,只有有需要的字段才会放置到目标序列中,比如那些为默认值的标量或者为NULL的向量就不会被放置到目标序列中去。table中的标量类型的字段默认值为NULL,向量类型的字段默认值也为NULL。

在table中,由于字段是可忽略的,保持字段的顺序是一件很重要的事情。调整字段的顺序会破坏向后兼容性,所以新增字段只能放置在最后面,并且不需要的字段不能被删除(但是可以标为deprecated)。

可以显示地给每个字段加上id属性,用于标识字段的顺序。这样字段在IDL中出现的顺序就不重要了,实际处理的时候会以id属性为准。一旦使用了id属性,table的所有的字段都必须加上id。

支持的类型

FBS内建对以下标量类型的支持:

  • 8 bit: byte (int8), ubyte (uint8), bool
  • 16 bit: short (int16), ushort (uint16)
  • 32 bit: int (int32), uint (uint32), float (float32)
  • 64 bit: long (int64), ulong (uint64), double (float64)

非标量类型可以有下面几种:

  • string,UTF-8或者7-bit ASCII的字符串
  • vector,任何除vector类型以外的向量(可以通过隔一层table来间接包含其他vector)
  • 到其他table、struct、enum、或union的引用

注意,一个字段的类型定义了就不能改了,但是一个变通的办法是在使用的时候通过reinterpret_cast来转换成其他值使用。

属性

  • id: n,用来给table的字段编号
  • deprecated,用来标识废弃的字段
  • required,只能用于非标量的table字段,表示一个字段是必须的(不需要去判断这个字段是否为NULL)
  • force_align: size,强制对齐一个struct
  • bit_flags,用于enum,让其中的枚举值按位递增,比如1,2,4,8…
  • nested_flatbuffer: "table_name"用于一个ubyte向量类型的字段,表示这个字段包含一个类型为"table_name"的FBS序列
  • flexbuffer用于一个ubyte向量类型的字段,表示这个字段可以看成Flexbuffer
  • key用于一个字段,表示这个字段可以用来作为排序时候使用的key
  • hash用于一个32或者64位的字段,表示这个字段会在JSON转码时保存JSON字符串的哈希值
  • original_order用于一个table,防止序列化的时候为了优化大小而改变table的字段顺序
  • native_*用于C++相关的特有的扩展。

IDL写作风格指导

  • Table, struct, enum的命名采用驼峰式命名: UpperCamelCase
  • Table和struct 的字段采用下划线方式命名:snake_case。这在生成某些语言源代码的时候会被转换成驼峰式命名,比如Java。
  • Enum的值采用驼峰式命名
  • namespaces采用驼峰式命名
  • 左花括号不不独处一行
  • 2空格缩进,:左右不带空格,=左右各带一空格

其他特性总结

  • 可以在一个FBS文件中包含另一个FBS文件,如include "mydefinitions.fbs";
  • 可以为一个FBS文件指定一个根类型:如root_type Monster,这有助于跟JSON的互操作
  • 可以指定一个四个字节的文件标识,如file_identifier "MYFI";,这个标识会放置在生成的序列中
  • 可以指定生成的序列文件的名字,如file_extension "ext";
  • table/struct/field/enum/union/element之前的///开头的注释会被拷贝到生成的代码中去
  • enum类型可以指定大小,如enum Color : byte ...
  • union类型默认带有一个隐藏的,以_type结尾的字段来标识union的类型,同时union类型不能作为Schema的根类型。
  • FBS支持和C++类似的命名空间(Namespaces)

其他类似的数据序列化工具

可以参考FlatBuffers的C++ Benchmarks

  • JSON
  • ProtoBuffers
  • Cap’n’Proto
  • msgpack
  • Apache Thrift
  • Project Anarchy (the free mobile engine by Havok)

其他参考