FlatBuffers C 语言开发指南:基于 FlatCC 的 schema 编译、Buffer 构建与反射实战

发布时间:2026/9/20 23:34:44
FlatBuffers C 语言开发指南:基于 FlatCC 的 schema 编译、Buffer 构建与反射实战 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 官方测试并支持的平台包括平台编译器 / 构建系统Ubuntuclang / gccninja / gnu makeOS-Xclang / gccninja / gnu makeWindowsMSVC 2010、2013、2015CI 会持续构建较新版本的 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 schemasamples/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_createFlatCC 的构建过程围绕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.mduint8_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一步创建若已有引用则用addxxx_create向量版对 C 数组直接用inventory_create一次写入配合c_vec_len计算元素个数Monster_weapons_push_create向武器向量中压入一个通过create快捷创建的 Weapon。注意push_create返回的是指向存储元素的短期指针因此用*解引用后赋值给axeWeapon_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的添加。读取 FlatBufferReader 接口读取时注意 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 默认值机制的标准行为。读取内嵌 structVec3时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向量与其中的 tablens(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))));读取 unionequipped时先取类型字段再按需把数据转换为具体类型。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)返回 1hp 被显式写入而Monster_mana_is_present(monster)返回 0mana 因等于默认值而被省略。该机制对区分字段缺失与字段为默认值的场景非常实用例如判断可选字段是否真的被写入。Union 的多种添加方式教程中我们用一次调用添加 union这里展示另外三种等价写法来自 docs/source/CUsage.mdns(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二进制 schemaFlatCC 的 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 ReflectionFlatCC 的 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来衔接两套代码生成体系以上三种方案都不够有吸引力。况且无论是否使用flatcFlatBuffers 的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),仅供参考