
FlatBuffers 实战教程从.fbsSchema 编写到多语言序列化与反序列化【免费下载链接】flatbuffersFlatBuffers: Memory Efficient Serialization Library项目地址: https://gitcode.com/GitHub_Trending/fl/flatbuffers本篇教程以 FlatBuffers 官方示例中的monster.fbs为贯穿始终的主线完整讲解如何编写 Schema、使用flatc编译器生成代码、将生成代码集成进应用、利用FlatBufferBuilder完成序列化以及通过零反序列化的方式读取二进制数据。读完本文你将掌握 FlatBuffers 从 schema 定义到多语言端到端使用的完整流程并理解 table、struct、vector、union、root_type 等核心类型系统在底层的真实行为。文中所有示例均以当前仓库 samples/monster.fbs 为基准并辅以 include/flatbuffers/flatbuffer_builder.h、tests/monster_test.cpp 等源码佐证实现细节。教程概览FlatBuffers 的五大步骤本教程覆盖在应用中从头使用 FlatBuffers 的全部环节整体流程如下编写 FlatBuffers Schema 文件.fbs定义待序列化数据结构的格式使用flatc编译器将 Schema 转换为特定语言的代码将生成的代码与运行时库导入应用序列化数据将对象写入 FlatBuffer 二进制缓冲反序列化访问FlatBuffer读取其中的数据。教程刻意保持语言无关的组织方式语言差异通过各语言代码块呈现。它覆盖 FlatBuffers 的主要组成部分与类型系统用于给出整体概览它并不追求穷举全部特性也不保证给出最优写法。FlatBuffers Schema.fbs使用 FlatBuffers 的第一步是创建 Schema 文件。Schema 定义了你希望序列化的数据结构格式由flatc编译器处理以生成可在项目中使用的、特定语言的代码。本教程使用下面的monster.fbsSchema它来自 FlatBuffers 的示例代码目录用于演示完整的示例二进制。FlatBuffers Schema 是一种接口定义语言IDLInterface Definition Language内含若干数据结构。完整语法细节见 Schema 文档 与 grammar 文档。下面通过行内代码注解快速说明 Schema 每个部分的作用。// Example IDL file for our monsters schema. namespace MyGame.Sample; //(1)! enum Color:byte { Red 0, Green, Blue 2 } //(2)! // Optionally add more tables. union Equipment { Weapon } //(3)! struct Vec3 { //(4)! x:float; //(5)! y:float; z:float; } table Monster { //(6)! pos:Vec3; //(7)! mana:short 150; //(8)! hp:short 100; name:string; //(9)! friendly:bool false (deprecated); //(10)! inventory:[ubyte]; //(11)! color:Color Blue; weapons:[Weapon]; //(12)! equipped:Equipment; //(13)! path:[Vec3]; } table Weapon { name:string; damage:short; } root_type Monster; //(14)!各注解说明namespace命名空间FlatBuffers 支持用命名空间把生成的代码放到指定位置。不同语言对命名空间的支持程度不一部分语言没有命名空间概念但对 C 系列语言C/C/C# 等有完整支持。enum枚举枚举可指定底层数值类型这里是byte。支持隐式编号因此Green自动取值为 1。union联合体union 表示从一组可能取值中选出单个值本质上是一个枚举记录实际存储的类型 一个值的组合。本例中 union 只有一个类型Weapon因此实用性有限仅用于演示。struct结构体struct 是带名字的标量字段集合它本身是标量类型内存占用更小、查找更快。但 struct 一旦定义就不可更改需要随时间演化的数据结构请使用 table。标量类型FlatBuffers 提供标准标量数值类型集合int8、int16、int32、int64、uint8、uint16、uint32、uint64、float、double以及bool。注意标量是固定宽度的不支持 varint变长整数。table表table 是组合数据的主要结构。它可以随时间演化——通过新增字段或弃用deprecated字段同时保持向前与向后兼容。struct 字段pos:Vec3是 struct 类型字段意味着Vec3的数据会被内联inline序列化在 table 中无需任何 offset 引用。默认值字段可以指定默认值。配置了默认值的字段在序列化时可以被完全省略反序列化时仍能返回该默认值。但默认值一旦设定就不能更改schema 演进时尤其要注意详见下文默认值小节。string 字段name:string指向一个序列化在 table 之外的字符串。deprecated 字段friendly:bool false (deprecated)表示该字段已弃用、不再使用用于替代直接删除字段的做法从而保持兼容性。vector 字段inventory:[ubyte]指向一段字节向量。与 string 类似向量数据序列化在别处此字段只保存指向向量的 offset。table/struct 向量weapons:[Weapon]说明 table 与 struct 的向量也是允许的。union 字段equipped:Equipment是 union 类型字段。root_type根类型FlatBuffer 的根对象永远是table。root_type Monster指明反序列化时缓冲区入口点所指向的 table 类型。从仓库源码看tests/monster_test.cpp中通过GetMonster(flatbuf)访问根对象并断言monster-hp() 80、monster-mana() 150默认值等正是对上述 Schema 语义的落地验证。三种字段缺失语义默认值 / Optional / RequiredSchema 文档进一步解释了 table 字段在二进制数据中缺失时的三种互斥反应见 docs/source/schema.md默认值Default按 Schema 中定义的默认值返回未显式指定默认值的标量类型返回0其他类型返回null。只有标量可以显式声明默认值string/vector/table 等非标量字段缺失时一律为null。注意不要随意修改已经发布的默认值值为默认值的字段实际上不会写入序列化数据旧 schema 代码写入的值若恰好等于默认值会被新 schema 代码读成不同值。Optional字段设为hp:short null后生产者未显式设置时该字段被标记为null语言侧返回null/std::optionalT。并非所有语言都支持标量默认值。Required字段标记(required)后若未被设置FlatBuffers verifier 会判定整个缓冲区无效。required不能与显式默认值同时使用否则编译报错。编译 Schema 生成代码flatcSchema 写好后需要把它编译成目标语言的代码。这一步由 FlatBuffers 编译器flatc完成它是仓库中构建出的二进制之一。构建flatcFlatBuffers 使用cmake为你的环境生成工程文件。Unix 环境下cmake -G Unix Makefiles make flatcWindows 环境下cmake -G Visual Studio 17 2022 msbuild.exe FlatBuffers.sln更详细的构建说明含 MacOS 的 Xcode 工程、FLATBUFFERS_STRICT_MODE严格模式、Bazel 构建等见 构建文档。部分语言也通过其包管理器提供预编译的flatc。编译 Schema调用flatc时传入 Schema 文件与你想生成代码的语言标志即可。编译会生成供应用包含的文件这些文件提供序列化与反序列化 FlatBuffer 二进制的便捷 API。为monster.fbs生成各语言代码的命令如下语言命令Cflatc --cpp monster.fbsC使用独立项目 FlatCC见下文C#flatc --csharp monster.fbsDartflatc --dart monster.fbsGoflatc --go monster.fbsJavaflatc --java monster.fbsJavaScriptflatc --js monster.fbsKotlinflatc --kotlin monster.fbsLobsterflatc --lobster monster.fbsLuaflatc --lua monster.fbsPHPflatc --php monster.fbsPythonflatc --python monster.fbsRustflatc --rust monster.fbsSwiftflatc --swift monster.fbsTypeScriptflatc --ts monster.fbs仓库 src 目录下的idl_gen_*.cpp如 idl_gen_cpp.cpp、idl_gen_go.cpp、idl_gen_python.cpp分别实现了各语言代码生成器感兴趣可深入阅读。C 语言的特别说明如果使用纯 C需要改用独立项目FlatCC含 C 语言用的 Schema 编译器与运行时库。请务必区分flatc与flatcc两个工具。FlatCC 的典型用法cd flatcc mkdir -p build/tmp/samples/monster bin/flatcc -a -o build/tmp/samples/monster samples/monster/monster.fbs # 或直接运行 flatcc/samples/monster/build.sh你可以用一种语言序列化、用另一种语言反序列化 FlatBuffer。为简化教程这里假设序列化与反序列化使用同一种语言。flatc的常用生成选项除语言标志外flatc还支持丰富的选项详见 flatc 文档-o PATH将所有生成文件输出到 PATH绝对或相对路径默认当前目录-I PATH指定include语句查找路径-b/--binary根据 Schema 将 JSON 数据文件序列化为二进制如flatc --binary myschema.fbs mydata.json生成mydata_wire.bin-j/--json反向将二进制转 JSON无file_identifier时需加--raw-binary--grpc同时生成 gRPC RPC 桩代码并非所有语言可用--gen-mutable生成可原地修改 FlatBuffer 的非 const 访问器--gen-object-api生成更便捷的对象式 API以对象分配为代价--gen-onefile为 C#/Go/Java/Kotlin/Python 生成单一输出文件--cpp-std CPP_STD选择 C 标准c0x/c11/c17--scoped-enums使用 C11 强类型枚举--strict-json、--defaults-json、--natural-utf8等控制 JSON 输入输出行为。应用集成生成的代码随后被包含进你的项目并编译进应用。这高度依赖你的构建系统与语言但通常包含两件事导入生成的代码由flatc生成导入运行时库各语言的 FlatBuffers runtime。以 C 为例其余语言见原文档对应的代码块#include monster_generated.h // 由 flatc 生成 #include flatbuffers.h // C 运行时库 // 简化下面的命名。 using namespace MyGame::Sample; // 在 Schema 中指定。运行时库的形态因语言而异有些语言只是需要编译进应用的代码文件例如 include/flatbuffers 下的 C 头文件、go 目录下的 Go 源码、python/flatbuffers 目录下的 Python 源码另一些语言则通过包管理器提供打包库如 Dart 的pubspec.yaml、Rust 的Cargo.toml。生成的代码同时包含序列化与反序列化 API因此生产方与消费方的集成步骤完全相同。序列化所有文件集成进应用后就可以开始序列化数据了。FlatBuffers 的序列化会略显繁琐每块数据必须单独、按特定顺序深度优先、先序遍历序列化。这种繁琐换来的是无需堆分配的、高效的序列化过程代价是序列化 API 更复杂。例如任何引用类型table、vector、string都必须在其被其他结构引用之前先序列化因此典型做法是从叶子节点向根节点逐层序列化如下文所示。FlatBufferBuilder大多数语言使用 Builder 对象管理数据序列化进入的二进制数组。它提供序列化数据的 API并维护一些内部状态生成的代码把 Builder 上的方法包装成语义贴合 Schema 的 API。首先实例化一个 Builder或复用已有实例并指定内存大小Builder 会在必要时自动扩容后备缓冲// 构造一个 1024 字节后备数组的 Builder。 flatbuffers::FlatBufferBuilder builder(1024);其余语言的构造方式C#new FlatBufferBuilder(1024)、Javanew FlatBufferBuilder(1024)、Goflatbuffers.NewBuilder(1024)、Pythonflatbuffers.Builder(1024)、RustFlatBufferBuilder::with_capacity(1024)、TypeScriptnew flatbuffers.Builder(1024)等。Builder 就绪后即可通过 Builder API 与生成的代码向其中写入数据。序列化数据本教程要为游戏构建Monster与Weapon。Weapon是 flatbuffertable含name:string字段与damage:short数值标量字段table Weapon { name:string; damage:short; }字符串Strings由于string是引用类型必须先序列化它才能赋给Weapon表的name字段这通过 Builder 的CreateString方法完成。先序列化两把武器的名字flatbuffers::OffsetString weapon_one_name builder.CreateString(Sword); flatbuffers::OffsetString weapon_two_name builder.CreateString(Axe);flatbuffers::Offset只是一个带类型的整数绑定到特定类型上让数值 offset 具备更强的类型约束。CreateString真正执行了序列化字符串数据被拷贝进后备数组并返回一个 offset——可以把它看作该引用的句柄一个指向数据在缓冲中位置的类型化数值偏移。从源码看flatbuffer_builder.h 中的CreateString实现支持传入 C 字符串或(char*, len)并返回OffsetString。表Tables有了名字之后可以序列化Weapon表。这里使用flatc生成的辅助函数CreateWeapon它接收 Builder、武器名字的 offset 以及damage数值short weapon_one_damage 3; short weapon_two_damage 5; // 使用 CreateWeapon() 快捷方式一次性创建字段齐全的 Weapon。 flatbuffers::OffsetWeapon sword CreateWeapon(builder, weapon_one_name, weapon_one_damage); flatbuffers::OffsetWeapon axe CreateWeapon(builder, weapon_two_name, weapon_two_damage);以 Go 为例生成代码给出的是Start/Add/End三段式 APIJava、Python、Lua 等语言类似两种风格完全等价sample.WeaponStart(builder) sample.WeaponAddName(builder, weaponOne) sample.WeaponAddDamage(builder, 3) sword : sample.WeaponEnd(builder)flatc生成的函数如CreateWeapon本质上由各种 Builder API 方法组合而成因此并非必须使用生成代码但生成代码让写法更简洁紧凑。与CreateString一样表序列化函数也返回指向序列化后Weapon表的 offset。字段序列化顺序没有规定哪些表字段必须先序列化你可以按任意顺序序列化也可以不序列化某字段使用 0 值 offset来表示null。有了Weapon可以继续序列化Monster。对照 Schema 看这个表字段更多、类型更杂其中一部分同样需要预先序列化原因与先序列化名字字符串一致table Monster { pos:Vec3; mana:short 150; hp:short 100; name:string; friendly:bool false (deprecated); inventory:[ubyte]; color:Color Blue; weapons:[Weapon]; equipped:Equipment; path:[Vec3]; }向量Vectorsweapons字段是Weapon表的vector。两把Weapon已序列化只需把这些 offset 序列化成一个vectorBuilder 提供多种创建vector的方式// 创建之前得到的 offset 的 std::vector。 std::vectorflatbuffers::OffsetWeapon weapons_vector; weapons_vector.push_back(sword); weapons_vector.push_back(axe); // 把 std::vector 序列化进缓冲再次得到一个指向该 vector 的 Offset。 // 完整类型很长这里用 auto它只是一个带类型的数值。 auto weapons builder.CreateVector(weapons_vector);C# / Java / TypeScript 等语言直接用生成辅助函数Monster.CreateWeaponsVector(builder, weaps)而 Go / Lua 由于 Builder 是向前prepend构建需要按逆序逐个PrependUOffsetTGobuilder.PrependUOffsetT(axe)再builder.PrependUOffsetT(sword)最后builder.EndVector(2)。顺带把另外两个向量字段也序列化了inventory是标量向量path是 struct本质也是标量数据向量因此可以直接序列化// 创建代表兽人背包的 vector每个数字对应击败他后可拾取的物品。 unsigned char treasure[] {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; flatbuffers::Offsetflatbuffers::Vectorunsigned char inventory builder.CreateVector(treasure, 10); // 构造两个 Vec3 struct 的数组。 Vec3 points[] { Vec3(1.0f, 2.0f, 3.0f), Vec3(4.0f, 5.0f, 6.0f) }; // 序列化为 struct 向量。 flatbuffers::Offsetflatbuffers::VectorVec3 path builder.CreateVectorOfStructs(points, 2);Python 侧用生成辅助与对象式构造Monster.CreateInventoryVector(builder, range(0, 10))path通过Vec3T列表调用Monster.CreatePathVector(builder, path_points)。联合体UnionsMonster表最后一个非标量字段是equippedunion。本例直接复用已序列化的Weaponunion 中唯一的类型无需重新序列化。union 字段会隐式添加一个隐藏的_type字段记录 union 中实际存储值的类型。序列化 union 时必须显式设置这个类型字段同时提供 union 值。同时把其余标量数据一并序列化因为我们已具备构造Monster所需的全部值与 offset// 创建 Monster 所需其余数据。 auto name builder.CreateString(Orc); // 创建位置结构体 auto position Vec3(1.0f, 2.0f, 3.0f); // 生命值 300法力值 150。 int hp 300; int mana 150; // 最后用 CreateMonster 辅助函数一次性设置所有字段。 // 这里通过之前序列化的 OffsetWeapon axe 的 .Union() 方法设置 union 字段 // 只需用自动生成的 Equipment_Weapon 枚举指明放入 union 的对象类型。 flatbuffers::OffsetMonster orc CreateMonster(builder, position, mana, hp, name, inventory, Color_Red, weapons, Equipment_Weapon, axe.Union(), path);Java / C# / Kotlin / Go / Python / TypeScript 等语言则使用StartMonster/addEquippedType(builder, Equipment.Weapon)/addEquipped(builder, axe)/EndMonster的显式风格Go 为MonsterAddEquippedType/MonsterAddEquipped。Rust 用Monster::create(mut builder, MonsterArgs{ ..., equipped_type: Equipment::Weapon, equipped: Some(axe.as_union_value()), ..Default::default() })。收尾Finishing至此名为 orc 的Monster已序列化进 flatbuffer并拿到了它的 offset。Schema 的root_type也是Monster万事俱备可以完成序列化步骤。在 Builder 上调用对应的finish方法传入 orc 的 offset指示该table是之后反序列化缓冲时的入口点// 调用 Finish() 告知 builder 该 monster 已完整。 // 也可以调用 FinishMonsterBuffer(builder, orc); builder.Finish(orc);各语言对应Gobuilder.Finish(orc)、Javabuilder.finish(orc)、Rustbuilder.finish(orc, None)、Swiftbuilder.finish(offset: orc)等。C 语言由于使用Monster_create_as_root创建无需额外的finish调用。Builder 一旦 finish就不能再向它序列化更多数据。访问缓冲Buffer Accessflatbuffer 现在可以被存储、发送到网络、压缩或做任何你想做的事。访问原始缓冲如下// 必须在 Finish() 之后调用。 uint8_t *buf builder.GetBufferPointer(); // 返回 GetBufferPointer() 指向的缓冲大小。 int size builder.GetSize();各语言取缓冲的方式略有差异详见原文档代码块Gobuilder.FinishedBytes()返回[]byteJava/Kotlin 用builder.dataBuffer()数据不从 0 开始而是buf.position()起长度为buf.remaining()或用sizedByteArray()拷贝一份Rustbuilder.finished_data()返回[u8]Pythonbuilder.Output()返回bytearrayDart 的builder.finish(orc)直接返回Uint8List。现在可以把字节写入文件或通过网络发送。缓冲在 Builder 被清空或销毁前一直有效。务必使用二进制模式文件模式或传输协议必须设为 BINARY 而非 TEXT。若以文本模式传输 flatbuffer缓冲会被破坏且难以排查。反序列化反序列化其实有点名不副实FlatBuffers 访问数据时不会整体反序列化缓冲它只解码被请求的数据其余数据保持原样。是否拷贝数据、甚至是否读取都由应用决定。这里继续用反序列化指代从二进制 flatbuffer 中访问数据。成功创建 orc FlatBuffer 后数据可保存、可传输。最终某个时刻需要访问缓冲获取底层数据。反序列化所需的应用集成与序列化完全相同见上文应用集成。根对象访问Root Access所有对 flatbuffer 数据的访问都必须先经过根对象每个 flatbuffer 只有一个根对象。生成代码提供根据缓冲获取根对象的函数uint8_t *buffer_pointer /* 你刚读取的数据 */; // 获取缓冲内根对象的视图。 Monster monster GetMonster(buffer_pointer);其他语言示例C#Monster.GetRootAsMonster(new ByteBuffer(bytes))JavaMonster.getRootAsMonster(ByteBuffer.wrap(bytes))Gosample.GetRootAsMonster(buf, 0)PythonMyGame.Sample.Monster.Monster.GetRootAs(buf, 0)Rustroot_as_monster(buf).unwrap()。Go/Python 的 offset 参数根访问函数通常传0这对大多数读取的缓冲都适用。若直接从builder.Bytes读取则需传入builder.Head()的 offset——因为 Builder 是**反向从尾部向前**构建缓冲的数据未必从 offset 0 开始。再次提醒务必以二进制模式读取字节否则缓冲可能损坏。在大多数语言中返回的对象只是带便捷访问器的数据视图数据通常不会从后备缓冲中拷贝出来这也意味着后备缓冲必须在视图存续期间保持存活。表字段访问Table Access查看flatc生成的文件会发现对每个table它都会为所有非deprecated字段生成访问器。例如Monster根表的部分访问器auto hp monster-hp(); auto mana monster-mana(); auto name monster-name()-c_str();这些访问器应分别返回300、150和Orc。默认值150并未存储在mana字段中我们仍能取回它因为生成的访问器在缓冲中找不到该字段时返回硬编码的默认值。这正是 FlatBuffers 的零开销 默认值省略设计值为默认值的字段不占字节。部分语言风格差异C#、Dart、Kotlin、Lobster、Swift 等以属性property方式暴露字段如monster.Hp、monster.hp其余语言以访问器方法方式暴露如monster.hp()。嵌套对象访问Nested Object Access访问嵌套对象的方式类似嵌套字段指向另一个对象类型。注意字段可能为null若未写入。例如访问Vec3类型的posstructauto pos monster-pos(); auto x pos-x(); auto y pos-y(); auto z pos-z();其中x、y、z将分别包含1.0、2.0、3.0。Go 有一个性能提示Pos()每次访问新对象都会创建临时访问器对象性能敏感时可传入已存在的Vec3指针替代nil以复用对象、减少分配与 GC。向量访问Vector Access同理可以用索引访问inventoryvector的元素也可遍历其长度flatbuffers::Vectorunsigned char inv monster-inventory(); auto inv_len inv-size(); auto third_item inv-Get(2);对应 Javamonster.inventoryLength()/monster.inventory(2)Lua 注意1 起始索引mon:Inventory(3)才是第三个元素。对于 table 的向量如weapons访问方式与普通向量类似但要把结果当作 FlatBuffer table 处理flatbuffers::VectorWeapon weapons monster-weapons(); auto weapon_len weapons-size(); auto second_weapon_name weapons-Get(1)-name()-str(); auto second_weapon_damage weapons-Get(1)-damage();联合体访问Union Access最后访问equippedunion字段。与创建 union 时一样需要同时取回 union 的两部分类型与数据。先取类型再据此对数据做动态转换union 只存储 FlatBuffertableauto union_type monster.equipped_type(); if (union_type Equipment_Weapon) { // 需要 static_cast 到类型 const Weapon*。 auto weapon static_castconst Weapon*(monster-equipped()); auto weapon_name weapon-name()-str(); // Axe auto weapon_damage weapon-damage(); // 5 }其他语言的等价做法Java/Kotlin 显式转型(Weapon)monster.equipped(new Weapon())Python 用flatbuffers.Table初始化Weapon对象union_weapon.Init(monster.Equipped().Bytes, monster.Equipped().Pos)Rust 用类型安全的方式monster.equipped_as_weapon().unwrap()若 union 实际不是该类型会返回NoneSwift 用monster.equipped(type: Weapon.self)。完整示例与验证上述整个序列化 → 读取闭环在仓库 samples/sample_binary.cpp 中有完整的可编译演示构造FlatBufferBuilder序列化两把武器、构建weapons向量、创建Monsterbuilder.Finish(orc)后用GetMonster(builder.GetBufferPointer())读回并以一系列assert校验hp 80、mana 150默认值、pos-z() 3.0f、inv-Get(9) 9、weapons向量元素及equippedunionAxe、damage 5。仓库还提供了 Pythonsamples/sample_binary.py、Gosamples/sample_binary.go、Javasamples/SampleBinary.java、C#samples/SampleBinary.cs、Lua、Swift、Rust 等多个语言的等价示例。在 tests/monster_test.cpp 中还能看到flatbuffers::Verifier与VerifyMonsterBuffer(verifier)的配合用法——生产环境在读取不受信任的数据前建议先用 verifier 校验缓冲完整性。另外tests/monsterdata_test.mon 是现成的序列化样例数据配合flatc --json可直观查看二进制内容对应的 JSON 形态。结语通过本教程你已走完 FlatBuffers 的完整主流程用.fbsIDL 描述数据结构table/struct/enum/union/vector/root_type用flatc编译出目标语言代码集成运行时库后借助FlatBufferBuilder以叶子到根的顺序序列化数据并Finish封口最终通过根对象访问器零拷贝地读取所需字段。这套模式在几乎所有受支持语言中保持一致差异仅体现在 API 命名风格方法 vs 属性、CreateXxxvsStart/Add/End与 Builder 构建方向如 Go/Lua 的逆序 prepend上。更深入的内容——如对象式 API、可变访问器、gRPC 支持、64 位 offset 等——可继续阅读 Schema 文档、flatc 文档 与 演进指南。【免费下载链接】flatbuffersFlatBuffers: Memory Efficient Serialization Library项目地址: https://gitcode.com/GitHub_Trending/fl/flatbuffers创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考