FlatBuffers 在 Dart 中的完整使用指南:从 flatc 代码生成到 Object API 实战
本篇技术指南以 FlatBuffers 官方文档中 Dart 语言专项章节(docs/source/languages/dart.md)为核心,结合仓库内 Dart 运行时源码与示例代码,系统讲解如何在 Dart/Flutter 项目中使用 FlatBuffers 完成高效的二进制序列化。读完本文,你将掌握通过 flatc 的 --dart 选项生成 Dart 代码、读取与构建 FlatBuffer、理解 Dart 实现与 Dart SDK 内置实现的差异,以及使用 --gen-object-api 生成更易用的 Object API 进行对象级读写。
前置准备:先掌握 FlatBuffers 通用基础
在使用 Dart 语言特性之前,需要先掌握 FlatBuffers 的通用工作流。官方建议按以下顺序阅读文档:
- 教程(Tutorial):包含所有受支持语言(含 Dart)的完整 FlatBuffers 通用用法指南,是理解本文的基础;
- 构建 flatc(Building):了解如何编译出
flatc命令行编译器; - schema 编译器使用(flatc):熟悉
flatc的各种生成选项; - schema 编写(Writing a schema):掌握 FlatBuffers IDL 的语法。
本页文档(dart.md)的定位是:在通用指南之上,专门覆盖 FlatBuffers 在 Dart 中的独有细节与差异。
FlatBuffers Dart 库的代码位置
Dart 运行时库代码位于仓库的 dart/ 目录,核心文件包括:
- dart/lib/flat_buffers.dart:运行时主库,包含
Builder(构建器)、BufferContext(缓冲上下文)、Reader系列(标量/字符串/列表读取器)等核心类; - dart/lib/flex_buffers.dart:FlexBuffers 的 Dart 实现;
- dart/lib/src/:其他辅助源码;
- dart/pubspec.yaml:包名
flat_buffers,版本 25.12.19,要求 Dart SDK>=2.17.0 <4.0.0,并依赖test、path、lints等开发依赖。
在 pubspec.yaml 的描述中可以看到,该实现"基于 Dart SDK 团队 Konstantin Scheglov 与 Paul Berry 的原始工作",这正是下文"与 Dart SDK 前端 flat_buffers 的差异"一节的由来。
测试 Dart 库:DartTest.sh 一键验证
Dart 库的测试代码位于 tests/ 目录,官方文档提到的测试入口是 dart_test.dart,但在当前仓库中,测试文件已组织在 dart/test/ 下(如 flat_buffers_test.dart、flex_builder_test.dart、flex_reader_test.dart、flex_types_test.dart 等),并配有多份 *_generated.dart 生成代码与 monsterdata_test.mon 二进制测试数据。
运行测试使用 tests/DartTest.sh 脚本(在 Windows 上可参考 DartTest.bat)。从脚本内容可以看到完整测试流程:
# 检查 Dart SDK 是否安装
command -v dart >/dev/null 2>&1 || {
echo >&2 "Dart tests require dart to be in path but it's not installed. Aborting."
exit 1
}
# 用 flatc 生成测试所需的 Dart 代码(--dart 与 --gen-object-api 同时启用)
../flatc --dart --gen-object-api -I include_test -o ../dart/test monster_test.fbs
../flatc --dart --gen-object-api -I include_test/sub -o ../dart/test include_test/include_test1.fbs
../flatc --dart --gen-object-api -I include_test -o ../dart/test include_test/sub/include_test2.fbs
# 复制测试二进制数据与 schema
cp monsterdata_test.mon ../dart/test
cp monster_test.fbs ../dart/test
cd ../dart
../flatc --dart --gen-object-api -o ./test ./test/enums.fbs
../flatc --dart --gen-object-api -o ./test ./test/bool_structs.fbs
# 更新依赖并执行测试
dart pub get
dart test
注意:脚本要求系统已安装 Dart SDK 并将 dart 命令加入 PATH,同时仓库根目录需存在可执行的 flatc。 测试脚本中使用的 --dart --gen-object-api 组合,正是本文后面要重点讲解的两个关键选项。
在 Dart 中使用 FlatBuffers 库
基本流程:flatc 生成 + 运行时读取
FlatBuffers 在 Dart 中同时支持**读取(reading)和写入(writing)**二进制 FlatBuffer。使用步骤分为两步:
- 用
flatc的--dart选项从 schema 生成 Dart 类,例如:flatc --dart monster.fbs - 在代码中同时引入运行时库与生成代码,即可读写 FlatBuffer。
读取 FlatBuffer 二进制文件
文档给出了读取 FlatBuffer 二进制文件的完整示例:先将二进制文件读入 List<int>,再传给生成类(如 Monster)的工厂构造函数:
import 'dart:io' as io;
import 'package:flat_buffers/flat_buffers.dart' as fb;
import './monster_my_game.sample_generated.dart' as myGame;
List<int> data = await new io.File('monster.dat').readAsBytes();
var monster = new myGame.Monster(data);
随后即可像访问普通 Dart 对象一样读取字段值:
var hp = monster.hp;
var pos = monster.pos;
对照仓库中的生成代码 dart/example/monster_my_game.sample_generated.dart,可以看到 Monster 的工厂构造函数内部通过 fb.BufferContext.fromBytes(bytes) 创建只读的缓冲上下文,再由 reader.read(rootRef, 0) 定位根对象:
factory Monster(List<int> bytes) {
final rootRef = fb.BufferContext.fromBytes(bytes);
return reader.read(rootRef, 0);
}
在运行时库 dart/lib/flat_buffers.dart 中,BufferContext.fromBytes 会把 List<int> 包装成 ByteData 视图(若传入的是 Uint8List 则直接复用其底层 buffer,零拷贝)。字段的读取依赖 Reader.vTableGet 通过 VTable 查找字段偏移:若字段不存在则返回默认值,这正是 FlatBuffers"字段可缺省、向后兼容"的核心机制。
写入 FlatBuffer:Builder 的两种风格
文档提到本实现的代码生成提供两类构建类:ObjectBuilder 与 Builder 类。仓库示例 dart/example/example.dart 中两种方式均有完整演示:
方式一:底层 Builder(贴近其他语言的 builders,内存更省)
final builder = fb.Builder(initialSize: 1024);
final int? weaponOneName = builder.writeString("Sword");
// ... 依次写入字符串、列表、struct、table
final int monsteroff = monster.finish();
builder.finish(monsteroff);
if (verify(builder.buffer)) {
print("The FlatBuffer was successfully created with a builder and verified!");
}
这种方式要求按预先顺序(pre-order)构造所有数据,即先写入嵌套的子对象(字符串、vector、struct),最后再构建引用它们的 table。从 MonsterBuilder 的生成代码可以看出,begin() 调用 fbBuilder.startTable(10),各 add* 方法调用 addInt16、addOffset、addStruct 等底层方法,finish() 调用 fbBuilder.endTable() 结束 table 并触发 VTable 去重。Builder 构造参数支持 initialSize(初始缓冲字节数,默认 1024)、internStrings(字符串驻留池)、deduplicateTables(VTable 去重,默认开启)与自定义 Allocator。
方式二:ObjectBuilder(更易用,代价是分配更多引用)
var monsterBuilder = my_game.MonsterObjectBuilder(
pos: my_game.Vec3ObjectBuilder(x: 1.0, y: 2.0, z: 3.0),
mana: 150,
hp: 300,
name: 'Orc',
inventory: [0, 1, 2, 3, 4, 5, 6, 7, 8, 9],
color: my_game.Color.Red,
weapons: [
my_game.WeaponObjectBuilder(name: 'Sword', damage: 3),
axe,
],
equippedType: my_game.EquipmentTypeId.Weapon,
equipped: axe,
);
var buffer = monsterBuilder.toBytes(); // 直接得到序列化后的 Uint8List
ObjectBuilder 的 toBytes() 内部会新建 Builder 并完成 finish,一次性返回可直接落盘或网络传输的字节流。运行时库中 ObjectBuilder 抽象类还提供了 getOrCreateOffset,允许复用同一 Builder 实例中已写入的偏移量。
两种风格对应运行时的两套核心类:
- 写入侧:
Builder负责从缓冲区尾部向头部反向写入,addField/_prepare处理对齐,endTable计算并去重 VTable(见 dart/lib/flat_buffers.dart 中_VTable类的_offsetsMatch逻辑,结构相同的表会共享同一 VTable); - 读取侧:
BufferContext提供各标量类型的_get*系列方法(全部按小端序Endian.little读取),Reader及其子类(Int32Reader、StringReader、ListReader等)负责类型化取值,列表读取器默认惰性读取(lazy),仅在访问元素时才解析,进一步降低反序列化开销。
与 Dart SDK 前端 flat_buffers 的关键差异
本仓库的实现大量借鉴了 Dart SDK front end/analyzer 包内部使用的实现,但做了若干显著改动,官方文档列出了五点,理解这些差异对于从 Dart SDK 迁移到本库尤为重要:
- 移除了布尔列表的打包(packed)支持。该特性在其他语言实现中并不标准、互不兼容。与 JavaScript 实现类似,布尔列表中的 null 值会被当作 false 处理。当然,仍然可以在单个标量字段内自行打包位数据,但这需要在应用侧手工完成。
- 枚举改用专门的枚举类。Dart SDK 实现使用普通 Dart 枚举,这仅在枚举总是从 1 开始索引时才正确;而 FlatBuffers 并不要求这一点。本实现采用类似枚举的专用类(见
EquipmentTypeId、Color的生成代码,每个常量包装一个value并附带fromValue工厂与values映射表),确保 FlatBuffers 与 Dart 及其他平台之间的映射正确。 - 完整支持 struct 与 struct 向量。SDK 实现似乎不支持 FlatBuffer struct 或 struct 向量,把所有东西都当作内建标量或 table;本实现以与其他非 Dart 实现兼容的方式处理 struct,并正确处理 struct 向量,为此改造了许多以
low前缀命名的方法。 - int64/uint64 不做浮点降级,并新增 16 位整数支持。SDK 实现把 int64/uint64 当作 float64 处理,本实现不会如此,这可能在 JavaScript 兼容性上带来问题——但可以通过直接使用 JavaScript 实现、或定制一个把所有 64 位数字当作浮点数的实现来规避。支持 Dart VM 与 Flutter 是本实现更重要的目标。这也解释了为何运行时库中
Uint64Reader带有"WARNING: May have compatibility issues with JavaScript"的注释(见 dart/lib/flat_buffers.dart)。 - 代码生成同时提供 ObjectBuilder 与 Builder 两类 API。ObjectBuilder 生成的代码与 SDK 中消费 FlatBuffers 的类非常相似,更易用,代价是额外分配更多对象引用;Builder 类则产出更接近其他语言 builder 风格的代码,内存效率更高。
文本解析(JSON/Schema)的限制
当前 Dart 实现尚不支持直接从 Dart 解析文本格式(包括 Schema 与 JSON)。如果需要文本解析能力,可以通过 Dart Native Extensions 调用 C++ 解析器实现——可参考仓库中 src/idl_parser.cpp(flatc 的 C++ 解析核心)与 src/idl_gen_text.cpp(文本输出)等 C++ 侧实现。需要说明的是:该方案目前不适用于 Flutter(受相关 Flutter 平台问题限制,详情可关注官方 issue 跟踪进展)。对于纯 Dart/Flutter 场景,建议直接消费二进制格式,或在服务端/构建期完成 JSON 到二进制的转换。
基于对象的 API(Object based API,--gen-object-api)
FlatBuffers 的立身之本就是内存效率,因此其基础 API 围绕"尽量少分配"设计——这导致 API 使用上较笨拙:要求预顺序构造所有数据,且变更(mutation)困难。
当效率不是首要考量时,可以通过 --gen-object-api 选项生成更便捷的基于对象的 API:它能把 FlatBuffer 解包(unpack)成普通对象与列表,从而支持便捷的构造、访问与变更;变更后再**打包(pack)**回新的 FlatBuffer。这一点也在 tests/DartTest.sh 中得到印证——测试生成代码时总是同时传入 --dart --gen-object-api。
典型使用方式(unpack → 修改 → pack)
文档给出了完整的三步用法:
// Deserialize from buffer into object.
MonsterT monster = Monster(flatbuffer).unpack();
// Update object directly like a Dart class instance.
print(monster.Name);
monster.Name = "Bob"; // Change the name.
// Serialize into new flatbuffer.
final fbb = Builder();
fbb.Finish(monster.pack(fbb));
以 MonsterT("T" 后缀即 Object API 生成的普通 Dart 类)为例,unpack() 将 FlatBuffer 展开为可直接读写的 Dart 对象;对属性直接赋值即完成修改;pack(fbb) 则把对象重新序列化进 Builder,最后由 fbb.Finish(...) 产出新的二进制缓冲。整个流程让"读-改-写"变成普通 Dart 对象操作,同时生成的二进制仍与其他语言实现完全互通。
底层机制对应
- 解包方向:生成类内部基于运行时库的
Reader/BufferContext(见 dart/lib/flat_buffers.dart)逐个字段读取,把 table/struct/vector 全部物化为 Dart 对象与List; - 打包方向:
pack利用Builder的writeString、writeList*、addOffset等 API 把对象图写回缓冲区,其中writeString支持asciiOptimization(纯 ASCII 字符串直接拷贝,避免utf8.encode的转换开销,见源码注释),并支持通过internStrings做字符串驻留去重。
与官方教程的衔接
本文聚焦 Dart 特有的细节;如需更深入、完整的端到端示例(schema 编写 → flatc 生成 → 各语言读写),请参阅 docs/source/tutorial.md。仓库中 dart/example/example.dart 提供了可直接运行的完整读写示例,dart/README.md 则说明了包的发布信息与 flatc 版本对应关系(建议下载与你所用 Dart 包版本匹配的 flatc)。Dart 运行时生成的代码与 C++/Java/Go 等其他语言实现完全二进制互通,这也是 FlatBuffers 跨平台序列化的核心价值所在。
总结
在 Dart 中使用 FlatBuffers 的要点可归纳为:
- 用
flatc --dart(可选叠加--gen-object-api)从 schema 生成代码; - 引入
package:flat_buffers/flat_buffers.dart运行时库; - 读取侧使用生成类的工厂构造函数 + VTable 驱动的惰性
Reader,高效且零分配访问字段; - 写入侧按"先子后父"的顺序用
Builder构建,或使用更便捷的 ObjectBuilder/Object API; - 牢记与 Dart SDK 内置实现的五点差异(布尔列表打包、枚举类、struct 支持、64 位整数、双 API 风格),避免迁移踩坑;
- 文本解析在纯 Dart 中暂不支持,需借助 C++ 解析器或提前转换。
这些能力使 Dart 与 Flutter 应用能够在移动端、桌面端与 Web 场景下,与其他语言无缝交换高效紧凑的二进制数据。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust4.2 K634
MiniCPM5-2BMiniCPM5-2B 是一款面向端侧、本地部署和资源受限场景的 2B 稠密 Transformer,能够达到同尺寸开源模型 SOTA 水平。Markdown300
jforgamejforgame是一个一站式游戏服务器开发框架。包含游戏服务器开发所需要的各种组件,比如网关,socket服务端与客户端,自定义高效消息编解码,游戏热更新,游戏通用工具等等。包含游戏服,跨服,匹配服,后台管理系统等实现,同时提供大量业务案例以供学习。亦可用于其他socket应用,例如及时聊天等。Java101
fizz-gateway-nodeAn Aggregation API Gateway in Java . FizzGate 是一个基于 Java开发的微服务聚合网关,是拥有自主知识产权的应用网关国产化替代方案,能够实现热服务编排聚合、自动授权选择、线上服务脚本编码、在线测试、高性能路由、API审核管理、回调管理等目的,拥有强大的自定义插件系统可以自行扩展,并且提供友好的图形化配置界面,能够快速帮助企业进行API服务治理、减少中间层胶水代码以及降低编码投入、提高 API 服务的稳定性和安全性。Java60
certd开源SSL证书管理工具;全自动证书申请、更新、续期;通配符证书,泛域名证书申请;证书自动化部署到阿里云、腾讯云、主机、群晖、宝塔;https证书,pfx证书,der证书,TLS证书,nginx证书自动续签自动部署JavaScript60
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python280