
FlatBuffers Schema 语言完全指南从 IDL 语法到高效数据建模【免费下载链接】flatbuffersFlatBuffers: Memory Efficient Serialization Library项目地址: https://gitcode.com/GitHub_Trending/fl/flatbuffers导读FlatBuffers 的 Schema 语言即 IDLInterface Definition Language是定义一切序列化数据结构的起点通过一个.fbs文件声明表table、结构体struct、枚举、联合体与 RPC 接口flatc编译器即可为 C、Java、Go、Python、Rust、Swift 等语言生成高效的类型安全代码。本文以仓库 docs/source/schema.md 为骨架结合 src/idl_parser.cpp、include/flatbuffers/flatbuffers.h 等实现源码与 tests/monster_test.fbs 真实测试 Schema完整讲解语法、字段行为、内建类型、属性attributes、JSON 解析规则与建模效率准则让你读完即可编写出可落地的生产级 Schema。一个完整的 Schema 示例Schema 语言的语法对任何熟悉 C 家族语言或常见 IDL 的开发者都很友好。以下示例是文档给出的完整 IDL 文件// 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; } table Weapon {} table Pickup {} root_type Monster;短短几行就覆盖了 Schema 的几乎全部核心元素namespace命名空间、attribute自定义属性声明、enum枚举、union联合体、struct结构体、table表、root_type根类型。仓库中的真实测试文件 tests/monster_test.fbs 是这一语法的完整工业级实践后续章节将频繁引用它。TablesFlatBuffers 的主要对象定义方式表是 FlatBuffers 中定义对象的主要方式由名称如Monster和一组字段组成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; }字段列表可以持续追加也可以废弃而不会破坏二进制兼容性——这正是 FlatBuffers 作为长期演进的数据格式的核心能力。字段Fields表字段由名称标识符、类型、可选的默认值、可选的属性attributes组成并以;结尾。完整的语法定义见 docs/source/grammar.md 的 EBNFfield_decl ident : type [ scalar ] metadata ;字段不必出现在线格式wire representation中构造对象时可以选择省略字段。这种按需写入的设计是 FlatBuffers 前向/后向兼容的机制新增字段不会使旧数据膨胀旧代码读取新数据、新代码读取旧数据都能正常工作。字段在二进制数据中缺失时有三种互斥的处理方式1. 默认值Default——常规模式未显式写入的字段返回 Schema 中定义的默认值如果 Schema 未指定默认值标量类型返回0其他类型返回nullmana:short 150; hp:short; inventory:[ubyte];这里mana缺失时返回150hp返回0inventory返回null。注意只有标量可以有显式默认值非标量字段字符串、向量、表缺失时一律为null。⚠️不要随意修改默认值值为默认值的字段实际上不会被写入序列化数据见下文 Gotchas。如果修改了默认值旧版本代码生成的数据中恰好等于旧默认值的字段会被新 Schema 的代码读成不同的值。将 optional 标量改为带默认值的标量时风险稍小非存在性不会被旧默认值污染但总体而言只有当你确信所有代码能同步重建时才考虑改动默认值。2. 可选值Optional——缺失即 null可选字段在生成的语言中返回某种形式的null例如 C 中为std::optionalT。对可选标量只需把默认值设为nullhp:short null;如果缓冲区生产者没有显式设置该字段它会被标记为null。仓库中的 tests/optional_scalars.fbs 完整演示了这一用法——同一个表中同时存在just_i8无默认值、maybe_i8: int8 null可选和default_i8: int8 42带默认值三种形态覆盖了全部整数、浮点、布尔与枚举类型。需要注意的是并非所有语言都已支持标量默认值null默认值。3. 必填Required——缺失即错误必填字段如果未被设置FlatBuffers 校验器verifier会将整个缓冲区判定为无效。通过required属性启用hp:short (required)required不能与显式默认值同时使用否则会产生编译错误。源码 src/idl_parser.cpp 印证了这些约束只有非标量表字段才能标记requiredonly non-scalar fields in tables may be required字段不能同时是 optional 和 requiredFields cannot be both optional and required.struct 的字段天然全部必填对其设置required属性是无意义的struct fields are always required。Structs零开销的定长内联结构struct 与 table 类似但所有字段都必须存在因此也没有默认值并且字段不能新增、不能废弃struct Vec3 { x:float; y:float; z:float; }struct 只能包含标量或其他 struct。它适合确定永远不变的简单对象如Vec3。相比 tablestruct 占用更少内存、访问更快它总是内联存储在父对象中且不使用虚拟表vtable。这在对性能敏感的场景如游戏中的坐标、数学向量非常关键。数组Arrays数组是定长元素集合的便捷简写并且与展开写法保持二进制等价普通写法数组写法struct Vec3 { x:float; y:float; z:float; }struct Vec3 { v:[float:3]; }数组目前仅支持在 struct 中使用。Types内建类型标量ScalarsFlatBuffers 提供全套固定尺寸标量没有变长整数如 varint大小有符号无符号浮点8 位byte,boolubyte(uint8)16 位short(int16)ushort(uint16)32 位int(int32)uint(uint32)float(float32)64 位long(int64)ulong(uint64)double(float64)括号内是别名例如uint8可以代替ubyte、int32可以代替int不影响代码生成。测试 Schema tests/monster_test.fbs 中的TypeAliases表逐一验证了这些别名int8、uint8、int16、float32、float64及对应向量。非标量Non-scalars向量Vectors任意类型的定长集合用[type]表示inventory:[ubyte];注意不支持向量嵌套向量。需要嵌套时把内层向量包进一个 tabletable nest { a:[ubyte] } table monster { a:[nest] }字符串Strings以长度前缀开头、零结尾的 UTF-8 或 7-bit ASCII 字符串name:string;字符串只允许 UTF-8 或 7-bit ASCII。对于其他文本编码或一般二进制数据应改用[byte]或[ubyte]向量。Enums命名常量序列枚举定义一系列命名常量每个给定值或在上一个基础上递增第一个值默认为0。通过:指定底层整数类型如byte该类型也决定了以此枚举为类型的字段的存储宽度enum Color : byte { Red 1, Green, Blue }只允许整数类型作为底层类型byte、ubyte、short、ushort、int、uint、long、ulong。枚举值只应新增不应删除枚举没有废弃机制。这意味着代码需要自行处理未知枚举值以实现前向兼容。测试文件中的Race枚举甚至允许负值起步None -1LongEnum则展示了 64 位ulong枚举的用法。Unions多类型联合Union 与枚举很像但它引用的不是常量名而是表名。声明 union 字段后还会自动生成一个带_type后缀的字段保存对应的枚举值运行时据此知道该把字段强转成哪种类型。可以给联合中的类型起别名使同一类型在不同名字下表达不同语义table PointPosition { x:uint; y:uint; } table MarkerPosition {} union Position { Start:MarkerPosition, Point:PointPosition, Finish:MarkerPosition }Union 包含一个特殊的NONE标记表示未存储任何值因此该名字不能用作别名。测试文件 tests/monster_test.fbs 演示了三种形式普通union Any { Monster, TestSimpleTableWithEnum, MyGame.Example2.Monster }、带别名的AnyUniqueAliases、以及同一类型多次出现的AnyAmbiguousAliases。Union 是发送多种消息类型的理想载体。注意因为 union 字段实际上是两个字段类型 值它必须属于某个 table不能单独作为 FlatBuffer 的根。如果需要更开放地区分不同 FlatBuffer例如作为文件格式应使用下文文件标识特性。实验性支持主要针对 C[Any]形式的 union 向量连同类型向量、union 中容纳 struct 和 string 等非表类型。标量不能直接进 union但可以包进 struct 而不增加任何空间开销。Namespaces命名空间与包namespace声明会为所有辅助代码生成对应的 C 命名空间、Java 包等用.指定嵌套命名空间/包namespace MyGame.Example;Includes跨文件复用可以在当前 Schema 中引入其他 Schema 文件include mydefinitions.fbs;这便于引用别处定义的类型。include会自动保证每个文件只被解析一次即使被多次引用。当用flatc生成代码时只生成当前文件中的定义被 include 文件中的类型需要单独生成。Root type根类型root_type声明序列化数据的根表。这对解析 JSON 尤为重要——JSON 本身不包含对象类型信息必须靠 Schema 的根类型来定位。root_type Monster;文件标识与扩展名File identification and extension通常 FlatBuffer 二进制缓冲区不是自描述的——解析前必须知道其 Schema。但如果把 FlatBuffer 当作文件格式使用就像大多数文件格式那样内置一个魔数来做 sanity check 会很方便。你可以自行在缓冲区前面加文件头但 FlatBuffers 提供了一种内建方式占用空间极小且与没有标识的缓冲区保持兼容。在 Schema 中声明file_identifier MYFI;标识符必须恰好 4 个字符最终落在缓冲区第 4-7 字节含。源码 src/idl_parser.cpp 中会校验file_identifier长度必须等于flatbuffers::kFileIdentifierLength即 4否则报错include/flatbuffers/flatbuffers.h 中的静态断言说明file_identifier大小被假定与uoffset_t相同这也是它恰好占据 4 字节的原因。对声明了标识符的 Schemaflatc在-b生成二进制时会自动写入标识符生成的FinishMonsterBuffer之类调用也会带上。如果指定了标识符却想生成不带标识符的缓冲区可以显式调用FlatBufferBuilder::Finish。加载缓冲区后可用MonsterBufferHasIdentifier之类的调用检查标识符是否存在。该特性最适合文件这类开放式用途。如果只是通过网络发送一组可能的消息之一用 union 更合适。另外flatc默认把二进制文件输出为.bin扩展名可用以下声明修改file_extension ext;RPC 接口声明Schema 中可以声明 RPC 调用定义一组函数入参request和出参response都必须是表类型rpc_service MonsterStorage { Store(Monster):StoreResponse; Retrieve(MonsterId):Monster; }生成什么代码、如何使用取决于目标语言和 RPC 系统。仓库对 gRPC 有初步支持通过--grpc代码生成器启用完整示例见 grpc/tests。tests/monster_test.fbs 中的MonsterStorage展示了流式与幂等扩展属性的实际写法streaming: none/server/client/bidi、idempotent对应的 gRPC 生成代码与测试分布在 tests/monster_test.grpc.fb.cc 等文件中。注释与文档注释注释写法与多数 C 系语言一致。此外**独占一行的三斜杠注释///**会被识别为该注释后一行声明table/struct/field/enum/union/元素的文档并输出到对应的 C 代码中每个声明允许有多行此类注释。测试 Schema 中就有大量真实用例例如/// an example documentation comment: monster object table Monster { ... }Attributes属性系统属性可以附加在声明字段/枚举值之后或 table/struct/enum/union 名字之后可以带值也可以不带。deprecated等属性由编译器理解用户自定义属性必须先用attribute声明如示例中的priority之后可在运行时解析 Schema 时查询——这对编写自己的代码生成器/编辑器等工具很有用例如附加帮助文本。编译器当前理解的属性如下id: n表字段手动设置字段编号。一旦使用该表所有字段都必须设置且编号必须是从 0 开始的连续区间。由于 union 类型实际占两个字段其id应为第二个字段的编号第一个是未在 Schema 中显式声明的类型字段例如 union 前一字段 id 为 6则 union 字段应为 8其类型字段隐式为 7。id允许字段在 Schema 中任意排序新增字段必须使用下一个可用 id。deprecated字段不再为该字段生成访问器代码应停止使用该数据。旧数据可能仍含该字段但新代码无法再访问。注意若废弃的是之前required的字段使用可选校验器verifier时旧代码可能无法通过新数据的校验。required非标量表字段该字段必须总是被设置。默认字段可以不出现有利于前向/后向兼容与数据结构灵活性指定该属性后读写双方都会把缺失视为错误。读取代码可直接访问字段而无需判空构造代码若未初始化该字段会触发 assertverifier 也会拒绝缺少必填字段的缓冲区。添加和移除该属性都可能破坏前向/后向兼容除非数据恰好总是设置该字段。force_align: sizestruct把该 struct 的对齐强制提高到其自然对齐以上前提是缓冲区确实按该对齐分配直接放在FlatBufferBuilder内的缓冲区不一定满足。注意与--object-api联用时当前不保证生效因为对象 API 可能按更低对齐分配对象。force_align: size向量把向量对齐强制改为与元素尺寸自然对齐不同的值。目前仅对生成的 C 代码生效。bit_flags无符号枚举枚举值表示位标志Schema 中指定的无符号值 N 实际代表1N不指定值则得到序列 1、2、4、8、……。tests/monster_test.fbs 中的Color:ubyte (bit_flags)与LongEnum:ulong (bit_flags)是标准示例Red 0即10Blue 3即13。nested_flatbuffer: table_name字段表明该字段必须是[ubyte]向量包含根类型为table_name的 FlatBuffer 数据生成代码会提供便捷访问器。flexbuffer字段表明该字段必须是[ubyte]向量包含 FlexBuffer 数据生成代码会提供访问 FlexBuffer 根的便捷方法。key字段该字段用作其所在表的向量排序键可用于就地二分查找。测试 Schema 中Ability.id、Stat.count、Monster.name都是key字段的实例。hash字段(无)符号 32/64 位整数字段JSON 解析时允许把字符串存为其哈希值。属性值指定哈希算法fnv1_32、fnv1_64、fnv1a_32、fnv1a_64。tests/monster_test.fbs 的Referrable.idhash:fnv1a_64及Monster表中testhashs32_fnv1到testhashu64_fnv1a一系列字段覆盖了全部四种算法。original_ordertable表元素无需按特定顺序存储通常为节省空间会按尺寸排序该属性阻止排序。一般没有理由使用它。native*一系列为支持 C 对象 APIObject Based API而添加的属性均以native*前缀命名如native_inline、cpp_type、cpp_ptr_type见 tests/monster_test.fbs 中native_inline:Test与各类引用字段。JSON 解析解析 Schema 的同一套解析器也能解析符合该 Schema 的 JSON 对象。与其他 JSON 解析器不同它是强类型的并直接解析进 FlatBuffer命令行用法见编译器文档 docs/source/flatc.md运行时用法见 C 文档。除需要 Schema 外它还有几处不同于常规 JSON 的解析行为接受带引号或不带引号的字段名输出时默认不带引号可通过strict_json标志强制带引号。枚举类型字段可识别符号值带不带引号均可如field: EnumVal整型字段也可用符号名但需带类型前缀且加引号如field: Enum.EnumVal。对位标志枚举可在同一字符串内用空格分隔多个值进行 OR如field: EnumVal1 EnumVal2或field: Enum.EnumVal1 Enum.EnumVal2。联合字段需像代码序列化一样用两个字段指定例如对字段foo必须在foo之前加foo_type: FooOneFooOne是你要使用的联合中的表。值为null的字段如field: null表示取该字段默认值等效于完全省略。内建转换函数例如在通常写3.14159的位置写rad(180)。当前支持rad、deg、cos、sin、tan、acos、asin、atan。解析 JSON 字符串时识别的转义码\n换行、\t制表、\r回车、\b退格、\f换页、\双引号、\\反斜杠、\/正斜杠\uXXXX16 位 Unicode 码点转换为等价 UTF-8 表示\xXX8 位十六进制数 XX。这是唯一不在 JSON 规范中的转义见 json.org但它是把任意二进制无损编入字符串所必需的例如标准 JSON 无法表示字节 0xFF。从二进制生成 JSON 时这些转义码也会被原样生成回来。数值解析比 JSON 更灵活格式更接近 C/C详见 docs/source/grammar.md整数字面量可有任意前导0但与 C/C 不同解析器忽略前导零而不视为八进制[081, -00094]等于十进制[81, -94]。接受无符号和有符号十六进制整数[0x123, 0x45, -0x67]等于十进制[291, 69, -103]。浮点格式与 C/C 完全兼容使用现代 C 编译器时还接受十六进制与特殊浮点字面量[-1.0, 2., .3e0, 3.e4, 0x21.34p-5, -inf, nan]。约定十六进制浮点的指数后缀p必须存在解析出的NaN会转换为无符号 IEEE-754 quiet-NaN。扩展浮点支持已在 x64 WindowsMSVC2015与 x64 LinuxLLVM 6.0、GCC 4.9上测试。为兼容 JSON lint 工具标量字段的所有数值字面量都可包成带引号字符串1,2.0,0x48A,0x0C.0Ep-1,-inf,true。建模准则效率EfficiencyFlatBuffers 的核心是效率但只有高效的 Schema 才能兑现这种效率。数据的表示方式通常有多种选择尺寸特性差异巨大现代习惯倾向把所有数据都表示成字典如 JSON因为灵活可扩展。FlatBuffers 虽可模拟键值表的向量但对强类型系统而言是糟糕的匹配会产生较大的二进制文件。FlatBuffers 的 table 比多数系统的类/struct 更灵活大量字段中只有少数被使用依然高效因此应尽可能用 table 而不是字典来组织数据。字符串作为值只在真正开放式场景使用能枚举就优先用枚举。FlatBuffers 没有继承表达一组相关数据结构的方式是 union。但 union 有成本若各结构相对相似、共享大量字段替代方案是单张包含所有字段的表——因为未出现的字段是廉价的。支持完整整数尺寸尽量选最小的够用尺寸而不是一律 int/long。缓冲区内的数据可以共享引用同一字符串/表把重复数据抽成独立数据结构可能很值得。风格指南Style guideSchema 中的标识符要翻译到多种编程语言因此套用你主力语言的风格通常不是好主意。建议遵循以下规范保证跨语言一致性各语言代码生成器会尽量基于 Schema 标识符生成符合该语言风格的标识符table、struct、enum、rpc 名类型UpperCamelCasetable/struct 字段名snake_case部分语言如 Java 会自动转为 lowerCamelCase枚举值UpperCamelCase命名空间UpperCamelCase格式约定次要但仍值得遵守左大括号与声明起始同行缩进 2 空格类型冒号两侧不加空格等号两侧加空格完整风格示例即本文开头的 Schema。Gotchas易踩的坑如何检测字段是否存在于表中多数序列化格式如 JSON、Protocol Buffers能明确表达对象中某字段是否存在从而把存在性当额外信息使用。FlatBuffers不会写入等于默认值的字段这带来显著的空间节省但代价是无法区分写入了默认值与根本没写。这只影响标量字段只有它们支持默认值未指定时默认 0。源码印证了这一行为include/flatbuffers/flatbuffer_builder.h 中AddElement的逻辑是如果值与默认值相同且未开启force_defaults就不写入而 include/flatbuffers/flatbuffers.h 的IsFieldPresent注释明确指出等于默认值的字段会返回 false除非使用了force_defaults。如果关心标量的存在性有三种方案可选标量optional scalars多数语言支持。把 Schema 中默认值设为null——null是所有类型之外的值因此只要调用add_field就总是写入。生成的字段访问器会使用该语言规范的可选类型如 C 的std::optionalT见 tests/optional_scalars.fbs。force_defaults部分FlatBufferBuilder实现提供该选项绕过不写默认值的行为之后可用IsFieldPresent查询存在性。用 struct 包装标量所有语言通用。把标量字段包进 struct缺失时返回 null。稍微不够顺手但 struct 不比它所代表的标量多占任何空间。小结Schema 语言是使用 FlatBuffers 的起点也是重中之重table/struct 的选择决定序列化性能与兼容性边界枚举/union 决定多态与消息路由能力属性系统则为工具链定制留足空间而 JSON 解析让 Schema 同时充当数据结构定义与数据交换格式。动手实践时可直接从 tests/monster_test.fbs 出发参照 docs/source/grammar.md 的 EBNF 语法再结合 docs/source/flatc.md 的编译器用法即可快速产出属于自己的高效 Schema。【免费下载链接】flatbuffersFlatBuffers: Memory Efficient Serialization Library项目地址: https://gitcode.com/GitHub_Trending/fl/flatbuffers创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考