FlatBuffers C 语言开发指南:基于 FlatCC 的 schema 编译、Buffer 构建与反射实战
【免费下载链接】flatbuffersFlatBuffers: Memory Efficient Serialization Library项目地址: https://gitcode.com/gh_mirrors/flat/flatbuffers
导读:本文聚焦 FlatBuffers 在 C 语言中的落地实践。C 语言绑定并不在本仓库内,而是由独立项目 FlatCC 提供,它包含 C 实现的 schema 编译器与运行时库,可以离线或在线生成代码、产出 buffer 校验器以及高速 JSON 解析/打印器,并与本仓库的
flatc工具保持兼容。读完本文,你将掌握如何用flatcc编译.fbsschema、以自底向上(bottom-up)与自顶向下(top-down)两种方式构建 FlatBuffer、读取与检查字段、添加 union、利用.bfbs二进制 schema 做运行时反射,以及理解 FlatCC 与flatc之间的关系与取舍。
C 语言绑定的定位:独立的 FlatCC 项目
FlatBuffers 主仓库(本仓库)提供的官方语言绑定覆盖 C++、Java、Go、Python、Rust、Swift 等(详见 docs/source/FlatBuffers.md),但C 语言绑定并不在主仓库内,它存在于一个独立项目中,名为FlatCC。
FlatCC 是一个"给 C 用的 FlatBuffers"项目,其核心能力包括:
flatccC schema 编译器:既可作为命令行工具离线生成代码,也可以作为 C 库在程序内部在线调用;- 生成 buffer 校验器(verifiers):用于在读取前验证 buffer 的合法性;
- 生成快速的 JSON 解析器与打印机:在 JSON 与 FlatBuffer 二进制之间互转;
- 与主
flatc项目保持兼容:FlatCC 在设计上对flatc生成的二进制格式做了细致的兼容处理,同一份 buffer 可以在两种生态间互通。
在 docs/source/Tutorial.md 的 C 语言章节中同样明确指出:"如果你在用 C 工作,你需要使用独立项目 FlatCC,它包含一个用 C 实现的、给 C 用的 schema 编译器与运行时库",并特别提醒开发者注意flatc与flatcc两个工具之间的区别。
注意:本文所有外部链接均指向 FlatCC 项目(其文档、samples、GitHub 仓库等),这些不在当前仓库范围内,此处仅作文字说明,不再列出具体 URL。读者可参考 FlatCC 项目自身的文档继续深入学习。
支持的平台与 C 标准要求
FlatCC 官方测试并支持的平台包括:
| 平台 | 编译器 / 构建系统 |
|---|---|
| Ubuntu | clang / gcc,ninja / gnu make |
| OS-X | clang / gcc,ninja / gnu make |
| Windows | MSVC 2010、2013、2015 |
CI 会持续构建较新版本的 gcc、clang 和 MSVC(覆盖 OS-X、Ubuntu、Windows),偶尔也会构建更老的编译器版本。其他平台(例如 CentOS)很可能也能正常工作,只是没有被定期测试。
一个重要的 C 标准细节:monster sample 项目是特意按 C99 编写的,目的是与 C++ 版本保持一致,因此它无法在 MSVC 2010 上编译(MSVC 2010 对 C99 支持不完整)。如果你需要在老 MSVC 上使用 FlatCC,需要留意这一点。
用flatcc编译 Schema(生成 C 代码)
与主仓库其他语言用flatc --xxx monster.fbs生成代码不同,C 语言使用flatcc工具。以 samples/monster.fbs 为例,docs/source/Tutorial.md 给出的 C 语言编译命令为:
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其中-a表示同时生成 builder(构建器)与 reader(读取器)代码,-o指定输出目录。当前仓库中的 monster schema(samples/monster.fbs)定义了本文全程使用的数据结构:
namespace MyGame.Sample; enum Color:byte { Red = 0, Green, Blue = 2 } union Equipment { Weapon } // Optionally add more tables. struct Vec3 { x:float; y:float; z:float; } 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]; } table Weapon { name:string; damage:short; } root_type Monster;编译后你会得到monster_builder.h(构建接口)与monster_reader.h(读取接口)等头文件。在 C 代码中包含并使用它们时,docs/source/Tutorial.md 给出的惯例写法是:
#include "monster_builder.h" // Generated by `flatcc`. // Convenient namespace macro to manage long namespace prefix. #undef ns #define ns(x) FLATBUFFERS_WRAP_NAMESPACE(MyGame_Sample, x) // Specified in the schema. // A helper to simplify creating vectors from C-arrays. #define c_vec_len(V) (sizeof(V)/sizeof((V)[0]))schema中的命名空间MyGame.Sample会被映射为宏前缀,ns(...)宏把所有生成函数统一包裹,避免每次写出超长的全限定名。
模块化对象创建:flatcc_builder_buffer_create
FlatCC 的构建过程围绕flatcc_builder_t展开。在 docs/source/Tutorial.md 的 C 语言示例中,最简单的用法是调用Monster_create_as_root一步完成"创建怪物并让它成为 buffer 根对象":
flatcc_builder_t builder, *B; B = &builder; // Initialize the builder object. flatcc_builder_init(B);随后用Monster_create_as_root(B, ...)一次性传入全部字段(struct、mana、hp、name、inventory、color、weapons、union、path),因为该调用自带as_root,之后不需要再调用finish。
但在更复杂的场景中,我们希望"创建嵌套 table"和"创建根 table"复用同一个函数,这时就需要模块化的写法。核心是使用flatcc_builder_buffer_create,并把flatcc_builder的调用隔离在顶层驱动函数中(flatcc_builder内部状态复杂,不宜在深层嵌套代码中混用),得到如下结构:
ns(Monster_ref_t) create_orc(flatcc_builder_t *B) { // ... same as in the tutorial. return s(Monster_create(B, ...)); } void create_monster_buffer() { uint8_t *buf; size_t size; flatcc_builder_t builder, *B; // Initialize the builder object. B = &builder; flatcc_builder_init(B); // Only use `buffer_create` without `create/start/end_as_root`. flatcc_builder_buffer_create(create_orc(B)); // Allocate and copy buffer to user memory. buf = flatcc_builder_finalize_buffer(B, &size); // ... write the buffer to disk or network, or something. free(buf); flatcc_builder_clear(B); }要点:
- 一旦使用了
flatcc_builder_buffer_create,就不能再混用create_as_root/start_as_root/end_as_root系列的根对象调用,二者只能取其一; flatcc_builder_finalize_buffer负责把内部缓冲区拷贝到用户内存并返回大小,之后调用free(buf)释放;flatcc_builder_clear释放 builder 内部资源,完成生命周期收尾;- 同样的原则也适用于
start/end与start/end_as_root这对调用:要么用start/end嵌套配合buffer_create,要么用start_as_root/end_as_root直接成根,不要混用。
自顶向下(Top-Down)构建示例
教程中其他语言大多采用自底向上的方式(先创建 Weapon,再创建 Monster)。而在 C 中,还可以使用自顶向下的方式:直接start根对象,然后在其中嵌套start/end各个子对象。由于教程示例嵌套不深,两种方式差别有限,但足以展示思路。完整示例(来自 docs/source/CUsage.md):
uint8_t treasure[] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9}; size_t treasure_count = c_vec_len(treasure); ns(Weapon_ref_t) axe; // NOTE: if we use end_as_root, we MUST also start as root. ns(Monster_start_as_root(B)); ns(Monster_pos_create(B, 1.0f, 2.0f, 3.0f)); ns(Monster_hp_add(B, 300)); ns(Monster_mana_add(B, 150)); // We use create_str instead of add because we have no existing string reference. ns(Monster_name_create_str(B, "Orc")); // Again we use create because we no existing vector object, only a C-array. ns(Monster_inventory_create(B, treasure, treasure_count)); ns(Monster_color_add(B, ns(Color_Red))); if (1) { ns(Monster_weapons_start(B)); ns(Monster_weapons_push_create(B, flatbuffers_string_create_str(B, "Sword"), 3)); // We reuse the axe object later. Note that we dereference a pointer // because push always returns a short-term pointer to the stored element. // We could also have created the axe object first and simply pushed it. axe = *ns(Monster_weapons_push_create(B, flatbuffers_string_create_str(B, "Axe"), 5)); ns(Monster_weapons_end(B)); } else { // We can have more control with the table elements added to a vector: // ns(Monster_weapons_start(B)); ns(Monster_weapons_push_start(B)); ns(Weapon_name_create_str(B, "Sword")); ns(Weapon_damage_add(B, 3)); ns(Monster_weapons_push_end(B)); ns(Monster_weapons_push_start(B)); ns(Monster_weapons_push_start(B)); ns(Weapon_name_create_str(B, "Axe")); ns(Weapon_damage_add(B, 5)); axe = *ns(Monster_weapons_push_end(B)); ns(Monster_weapons_end(B)); } // Unions can get their type by using a type-specific add/create/start method. ns(Monster_equipped_Weapon_add(B, axe)); ns(Monster_end_as_root(B));这个例子展示了大量 FlatCC 生成 API 的命名规律:
Monster_start_as_root/Monster_end_as_root:成对使用,二者缺一不可(注释明确强调"如果使用end_as_root,就必须用start_as_root开始");xxx_create_strvsxxx_add:字符串字段,若还没有现成的 string 引用(offset),直接用create_str一步创建;若已有引用则用add;xxx_create(向量版):对 C 数组直接用inventory_create一次写入,配合c_vec_len计算元素个数;Monster_weapons_push_create:向武器向量中压入一个通过create快捷创建的 Weapon。注意push_create返回的是指向存储元素的短期指针,因此用*解引用后赋值给axe(Weapon_ref_t),以便后续复用;push_start/push_end分步版:else分支展示了更细粒度的控制——先push_start开始一个元素,用Weapon_name_create_str、Weapon_damage_add填充字段,再push_end结束该元素;- union 的类型专用方法:
Monster_equipped_Weapon_add(B, axe)一次同时完成 union 的类型(Weapon)与数据(axe)的添加。
读取 FlatBuffer:Reader 接口
读取时,注意 FlatCC 的类型后缀约定:构建时用ref_t后缀(引用),读取时用table_t后缀(访问器)。从 buffer 中取出根对象(docs/source/Tutorial.md):
// Note that we use the `table_t` suffix when reading a table object // as opposed to the `ref_t` suffix used during the construction of // the buffer. ns(Monster_table_t) monster = ns(Monster_as_root(buffer)); // Note: root object pointers are NOT the same as the `buffer` pointer.之后可以读取标量字段:
uint16_t hp = ns(Monster_hp(monster)); uint16_t mana = ns(Monster_mana(monster)); flatbuffers_string_t name = ns(Monster_name(monster));这些值应当分别是300、150和"Orc"。教程特别提示:mana的默认值是 150,而默认值并不会被写入 buffer,但读取时依然能拿到 150——这是 FlatBuffers 默认值机制的标准行为。
读取内嵌 struct(Vec3)时,Monster_pos返回Vec3_struct_t,再用Vec3_x/y/z取分量:
ns(Vec3_struct_t) pos = ns(Monster_pos(monster)); float x = ns(Vec3_x(pos)); float y = ns(Vec3_y(pos)); float z = ns(Vec3_z(pos));读取weapons向量与其中的 table:
ns(Weapon_vec_t) weapons = ns(Monster_weapons(monster)); size_t weapons_len = ns(Weapon_vec_len(weapons)); // We can use `const char *` instead of `flatbuffers_string_t`. const char *second_weapon_name = ns(Weapon_name(ns(Weapon_vec_at(weapons, 1)))); uint16_t second_weapon_damage = ns(Weapon_damage(ns(Weapon_vec_at(weapons, 1))));读取 union(equipped)时,先取类型字段,再按需把数据转换为具体类型。C 语言允许 void 指针静默赋值,因此无需显式强转:
// Access union type field. if (ns(Monster_equipped_type(monster)) == ns(Equipment_Weapon)) { // Cast to appropriate type: // C allows for silent void pointer assignment, so we need no explicit cast. ns(Weapon_table_t) weapon = ns(Monster_equipped(monster)); const char *weapon_name = ns(Weapon_name(weapon)); // "Axe" uint16_t weapon_damage = ns(Weapon_damage(weapon)); // 5 }检查字段是否存在:_is_present
并非所有语言都支持"字段是否被显式写入"的测试,但 C 可以。教程中mana被设置为默认值150,因此它不应该出现在 buffer 中(字段被省略)。可以用_is_present验证这一点:
int hp_present = ns(Monster_hp_is_present(monster)); // 1 int mana_present = ns(Monster_mana_is_present(monster)); // 0Monster_hp_is_present(monster)返回 1(hp 被显式写入),而Monster_mana_is_present(monster)返回 0(mana 因等于默认值而被省略)。该机制对区分"字段缺失"与"字段为默认值"的场景非常实用,例如判断可选字段是否真的被写入。
Union 的多种添加方式
教程中我们用一次调用添加 union,这里展示另外三种等价写法(来自 docs/source/CUsage.md):
ns(Equipment_union_ref_t) equipped = ns(Equipment_as_Weapon(axe)); ns(Monster_equipped_add(B, equipped)); // or alternatively ns(Monster_equipped_Weapon_add(B, axe)); // or alternatively ns(Monster_equipped_add_type(B, ns(Equipment_Weapon)); ns(Monster_equipped_add_member(B, axe));- 方式一:先用
Equipment_as_Weapon(axe)把 table 引用包装成 union 引用,再通过通用的Monster_equipped_add一次性添加; - 方式二:使用类型专用的
Monster_equipped_Weapon_add,一步完成类型与数据添加(自顶向下示例中采用的就是这种方式); - 方式三(底层写法):拆分为
Monster_equipped_add_type(只加类型)与Monster_equipped_add_member(只加数据)两次调用。这种形式很少用,但它可以把类型和数据在不同时间点分别加入 table,从而把较小的值在表中就近分组存放,属于更底层的控制手段。
反射(Reflection):读取.bfbs二进制 schema
FlatCC 的 C API支持读取二进制 schema(.bfbs)文件,其原理与本仓库的反射机制同源。在本仓库中,.bfbs二进制 schema 对应的是reflection这个"元 schema"——一份描述 schema 自身的 schema,位于 reflection/reflection.fbs。
查看该文件可以看到,它定义了BaseType枚举(None、UType、Bool、Byte、UByte、Short、UShort、Int、UInt、Long、ULong、Float、Double、String、Vector、Obj、Union、Array、Vector64等),以及Type、Field、Object、Enum、Service、Schema等表,根类型为Schema,文件标识符为"BFBS",扩展名为"bfbs"。也就是说,任意一个.fbsschema 经flatc编译后,都可以输出一份符合reflection.fbs定义的二进制 FlatBuffer,从而在运行时被反射解析。
如何生成.bfbs文件?docs/source/Compiler.md 中的flatc选项说明指出:
--schema:序列化 schema 而不是 JSON(需与-b配合使用),输出该 schema 的二进制版本,其结构对应reflection/reflection.fbs;加载这份二进制文件是反射功能的基础;--bfbs-comments:向二进制 schema 文件中加入文档注释。
在 FlatCC 中,反射相关的头文件(由reflection.fbs生成的代码)已预先包含在 FlatCC 的运行时发行包中,并配有示例程序演示如何遍历二进制 schema、按名称查找对象等操作。这与本仓库中 include/flatbuffers/reflection.h、include/flatbuffers/reflection_generated.h 提供的 C++ 反射头文件在思路上是一致的(C++ 反射的更多细节见 docs/source/CppUsage.md 的 "Reflection (& Resizing)" 一节)。
变更与反射(Mutations and Reflection)
FlatCC 的 C API 在"变更"能力上有明确的边界:
- C API 不支持像 C++ 那样的反射式修改(mutating reflection);同时,reader 接口也不支持修改标量值(即使在验证之后直接改写标量通常也是不安全的);
- 生成的 reader 接口支持对向量进行原地排序(sort in-place):做法是把向量强制转换为可变更类型后再排序。之所以需要这样做,是因为在构建 buffer 的过程中做排序并不现实。排序功能在 FlatCC 的 builder 文档中有详细说明,前述反射示例就利用了这一特性,通过名称查找对象;
- 可以用已有 buffer 中的复杂对象作为源来构建新 buffer:由于是直接拷贝语义,无需进行端序转换,也不需要临时栈分配,因此效率很高。可以作为源的数据包括:标量、struct、string,以及这些类型的向量;
- 目前尚不支持把已有的 table 或 table 向量直接作为源使用,但文档指出未来有可能增加这一支持。
命名空间处理:FLATBUFFERS_WRAP_NAMESPACE与直接前缀
教程中使用的FLATBUFFERS_WRAP_NAMESPACE宏在函数命名空间前缀很长时非常方便:
#define ns(x) FLATBUFFERS_WRAP_NAMESPACE(MyGame_Sample, x)ns(Monster_hp(monster))会被展开成完整的MyGame_Sample_Monster_hp(monster)之类的调用。但它并非永远是最佳选择:如果命名空间不存在,或者前缀简单且信息量足,完全可以直接使用完整前缀而不经过宏包装。FlatCC 的反射示例(将.bfbs转换为 JSON 的程序)采用的就是直接使用前缀的写法,代码更直白、更利于阅读和搜索。
为什么 C 代码生成器不集成进flatc工具?
一个常见疑问是:为什么 C 语言绑定不在主仓库的flatc中直接集成?docs/source/CUsage.md 对此给出了明确的权衡分析:
- 若要集成,要么放弃 FlatCC 独立实现的 C 版 schema 编译器(它是 FlatCC 的根基),要么导致大量代码重复,要么不得不发明一套复杂的中介表示(IR)来衔接两套代码生成体系;
- 以上三种方案都不够有吸引力。况且,无论是否使用
flatc,FlatBuffers 的C 运行时库都需要单独提供,所以直接用flatcc工具替代flatc并没有额外负担——这一决定在 docs/source/IntermediateRepresentation.md 所述的主仓库 IR 设计中也可以得到侧面印证:主仓库各语言的代码生成共用 IR,而 C 绑定选择了独立于该 IR 的另一种实现路径。
结语
概括而言,在 FlatBuffers 生态中使用 C 语言,实际上就是使用与主仓库二进制格式兼容的FlatCC工具链:用flatcc -a编译 schema 得到 builder/reader 头文件;用flatcc_builder以自底向上或自顶向下的方式构建 buffer(注意as_root系列与buffer_create两种模式的互斥约定);用_is_present判断字段是否真实存在;用.bfbs二进制 schema 与预生成的反射代码实现运行时反射;并遵守"不支持标量变更、仅支持向量原地排序、可拷贝复用已有子对象"的能力边界。这些 API 命名规律(ref_t/table_t后缀、_add/_create_str/_push_create语义)与设计约定,配合本仓库的 samples/monster.fbs 与 docs/source/Tutorial.md C 语言章节,可以让你快速上手 C 语言下的 FlatBuffers 序列化与反序列化开发。
【免费下载链接】flatbuffersFlatBuffers: Memory Efficient Serialization Library项目地址: https://gitcode.com/gh_mirrors/flat/flatbuffers
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考