拓冰建站拓冰建站
首页 / 资讯中心 / 正文

FlatBuffers C 语言使用指南:基于 FlatCC 的 Schema 编译、构建器与反射实战

FlatBuffers C 语言使用指南基于 FlatCC 的 Schema 编译、构建器与反射实战【免费下载链接】flatbuffersFlatBuffers: Memory Efficient Serialization Library项目地址: https://gitcode.com/GitHub_Trending/fl/flatbuffers导读本文以 FlatBuffers 官方文档中的 C 语言绑定指南docs/source/languages/c.md为核心讲解如何在 C 项目中通过独立的FlatCC工具链完成 schema 编译、缓冲区构建含模块化与自顶向下两种模式、二进制 schema.bfbs反射、字段存在性检测与 Union 多方式写入等完整实战技能。读完本文你将能够脱离flatc独立地在 C 工程中序列化、验证与反射 FlatBuffers 数据。C 语言绑定为什么选择 FlatCCFlatBuffers 主项目flatc原生支持 C、Java、C#、Go、Python、Rust 等语言的代码生成但C 语言绑定并不在主仓库内而是存在于一个独立项目 FlatCCflatcc即 FlatBuffers in C for C中。这是一个针对纯 C 场景设计的独立实现包含两大部分C schema 编译器flatcc命令行工具支持离线代码生成也支持通过 C 库以在线程序内方式调用编译能力C 运行时库除生成代码外还可生成缓冲区校验器buffer verifiers、快速 JSON 解析器与输出器printers。FlatCC 在实现上对主项目flatc保持了高度兼容——官方文档明确说明Great care has been taken to ensure compatibility with the mainflatcproject即生成的二进制布局与flatc产物互通这一点从主仓库教程中给出的编译命令差异flatc --cpp对flatcc -a可以印证参见 docs/source/tutorial.md。支持平台根据文档FlatCC 的 CI 持续构建覆盖以下平台与编译器组合平台编译器 / 构建系统Ubuntuclang / gcc配合 ninja / gnu makeOS-XmacOSclang / gcc配合 ninja / gnu makeWindowsMSVC 2010 / 2013 / 2015CI 会构建较新版本的 gcc、clang 与 MSVC偶尔也会测试较老版本编译器。其他平台如 CentOS通常也能正常工作但官方说明其未经过定期测试。需要特别注意的是文档中的monster 示例特意按 C99 标准编写以尽量贴近 C 版本的教程代码因此该示例无法在 MSVC 2010 上编译。快速上手编译 Schema 生成 C 代码C 语言并不通过flatc生成代码而是使用独立的flatcc工具。主仓库教程给出了标准流程见 docs/source/tutorial.mdcd 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指定输出目录。请注意flatc与flatcc是两个不同的工具不要混用。示例使用的 schema 与主仓库教程一致即 samples/monster.fbs它定义了namespace MyGame.Sample、枚举Color、联合Equipment、结构体Vec3、表Monster与Weapon并以root_type Monster声明根类型。生成完成后工程中会得到以monster_builder.h为代表的头文件——教程中 C 的集成示例直接#include monster_builder.h见 docs/source/tutorial.md。应用集成命名空间宏与辅助宏将生成代码纳入工程后通常需要两样东西生成代码本身以及 FlatCC 的运行时库。C 侧还常用两个宏来简化书写#include monster_builder.h // 由 flatcc 生成。 // 便捷的命名空间宏用于管理冗长的命名空间前缀。 #undef ns // 命名空间在 schema 中指定MyGame.Sample。 #define ns(x) FLATBUFFERS_WRAP_NAMESPACE(MyGame_Sample, x) // 辅助宏从 C 数组推导向量长度。 #define c_vec_len(V) (sizeof(V)/sizeof((V)[0]))FLATBUFFERS_WRAP_NAMESPACE是 FlatCC 提供的包装宏把ns(Monster)展开为MyGame_Sample_Monster一类的完整符号。在后续所有代码中ns(...)都代表对生成符号的命名空间包装调用。模块化对象创建flatcc_builder_buffer_create教程中的简单用法是直接调用Monster_create_as_root一步创建根缓冲区对象。但当我们需要把创建嵌套表和创建根表的逻辑复用到同一个函数中时就需要更高的模块化。为此FlatCC 提供了flatcc_builder_buffer_create调用。最佳实践是将flatcc_builder的调用尽量隔离在顶层驱动代码中业务层只负责返回对象引用。官方示例ns(Monster_ref_t) create_orc(flatcc_builder_t *B) { // ... 与教程中相同。 return s(Monster_create(B, ...)); } void create_monster_buffer() { uint8_t *buf; size_t size; flatcc_builder_t builder, *B; // 初始化 builder 对象。 B builder; flatcc_builder_init(B); // 只使用 buffer_create不要混用 create/start/end_as_root。 flatcc_builder_buffer_create(create_orc(B)); // 分配并拷贝缓冲区到用户内存。 buf flatcc_builder_finalize_buffer(B, size); // ... 将缓冲区写入磁盘或网络或做其他处理。 free(buf); flatcc_builder_clear(B); }这里的关键约束是一旦改用flatcc_builder_buffer_create不要再同时使用create/start/end_as_root系列调用两者是互斥的两套完成根缓冲区的方式。同样的原则也适用于自顶向下top-down方式中的start/end与start/end_as_root的区别。关于 builder 的生命周期主仓库教程补充了完整细节见 docs/source/tutorial.mdflatcc_builder_finalize_buffer(B, size)会从 builder 内部堆中分配并抽取一份可读缓冲区返回的缓冲区必须用free释放finalize 并不会改变 builder 本身它只是对 builder 内容做了一次快照之后可用flatcc_builder_reset(B)在不释放内部栈与堆的前提下重置 builder以便复用来构建下一个缓冲区最终用flatcc_builder_clear(B)完成清理。自顶向下Top-Down构建示例教程默认采用自底向上bottom-up的构建方式先创建叶子对象字符串、向量、子表再逐层向上组装。而在 C 中同样可以采用自顶向下方式先start外层对象再在其内部嵌套start/end内层对象。对于没有深层嵌套的 monster 示例两者差异有限但足以展示思路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; // 注意如果使用 end_as_root则必须同时以 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)); // 使用 create_str 而非 add因为这里没有现成的字符串引用。 ns(Monster_name_create_str(B, Orc)); // 同样使用 create因为这里没有现成的向量对象只有 C 数组。 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)); // 稍后要复用 axe 对象。注意这里对指针解引用 // 因为 push 总是返回指向已存元素的短期指针。 // 也可以先创建 axe 对象再直接 push。 axe *ns(Monster_weapons_push_create(B, flatbuffers_string_create_str(B, Axe), 5)); ns(Monster_weapons_end(B)); } else { // 对加入向量的表元素我们可以获得更细粒度的控制 // 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)); } // Union 可以通过类型专用的 add/create/start 方法获取其类型。 ns(Monster_equipped_Weapon_add(B, axe)); ns(Monster_end_as_root(B));这段代码体现了几个重要的 API 约定*_add用于添加已存在的对象引用*_create用于直接基于 C 值创建并添加push系列调用返回的是指向已存储元素的短期指针因此需要解引用*才能拿到可长期持有的ref_tif (1) { ... } else { ... }分支展示了简化路径与细粒度控制路径两种写法两者等价由于使用了Monster_start_as_root最终必须以Monster_end_as_root收尾且不再需要单独的 finish 调用——教程 C 部分明确指出因为使用了Monster_create_as_rootC 中不需要 finish 调用docs/source/tutorial.md。基本反射读取二进制 Schema.bfbsFlatCC 的 C API支持读取二进制 schema.bfbs文件其实现方式是从reflection.fbsschema 生成对应代码主仓库中该 schema 位于 reflection/reflection.fbs再以这些生成代码为媒介解析.bfbs。FlatCC 的运行时发行包中预生成了这些反射 schema 文件开箱即用。典型用途是把一个.fbs编译出的.bfbs运行时读入动态获取其中的表、字段、类型信息从而实现类似按名字查找字段的通用工具如把任意 buffer 转成 JSON。变更Mutation与反射的边界文档对 C API 的变更能力给出了明确边界不支持像 C 那样的反射式修改mutating reflectionreader 接口不支持原地修改标量字段——即使经过验证对已生成 buffer 做标量原地修改通常也是不安全的。但有一项例外生成的 reader 接口支持在把向量强转为可变类型后对向量进行原地排序sort。之所以放在 reader 侧而非构建期是因为在构建过程中对向量排序不切实际。上述反射示例正是利用这一特性实现了按名称查找对象的能力。此外FlatCC 支持以既有 buffer 中的复杂对象作为源数据构建新缓冲区其高效之处在于直接拷贝语义无需进行字节序转换也无需临时栈分配。可作为源数据的类型包括标量scalars、结构体structs、字符串strings上述类型的向量vectors。目前尚不支持直接以已有的 table 或 table 向量作为源数据但文档指出未来有增加此支持的可能。命名空间的处理策略教程中使用的FLATBUFFERS_WRAP_NAMESPACE包装方式即#define ns(x) FLATBUFFERS_WRAP_NAMESPACE(MyGame_Sample, x)在每个函数名前缀都非常长时很方便但它并不总是最佳选择。如果命名空间本身缺失或者命名空间简单且具信息量直接使用完整前缀反而更清晰。文档提到的反射示例bfbs2json.c正是采用直接使用前缀的方式。实践建议项目中使用ns()宏还是直接写全名取决于符号的冗长程度与代码可读性需求两种风格在 FlatCC 生成代码中都被完全支持。检查字段是否存在Present Members并非所有语言都支持测试字段是否已显式写入 buffer但C 可以。在教程的读取示例中mana被设置为默认值150因此它不应被视为存在schema 中mana:short 150的默认值语义见 samples/monster.fbs。存在性检测示例int hp_present ns(Monster_hp_is_present(monster)); // 1 int mana_present ns(Monster_mana_is_present(monster)); // 0hp显式写入了300故Monster_hp_is_present返回 1mana使用默认值150Monster_mana_is_present返回 0。这一机制对字段演进与可选字段判断非常有用可以区分字段未写入使用默认值与字段被显式写入两种状态。添加 Union 的三种等价写法教程中演示了用单次调用添加 union。这里展示三种达成同样效果的方式——最后一种属于底层写法使用频率较低但它允许把类型与数据在不同时间点分别写入从而在表中把小的值聚合在一起例如将多个 union 的 type 字段集中存放ns(Equipment_union_ref_t) equipped ns(Equipment_as_Weapon(axe)); ns(Monster_equipped_add(B, equipped)); // 或者 ns(Monster_equipped_Weapon_add(B, axe); // 或者底层写法 ns(Monster_equipped_add_type(B, ns(Equipment_Weapon)); ns(Monster_equipped_add_member(B, axe));三种写法最终产生的二进制内容一致Equipment_as_Weapon(axe)先把已有武器引用包装成 union 引用再用Monster_equipped_add一次性写入类型与成员Monster_equipped_Weapon_add是类型专用的便捷方法一步完成Monster_equipped_add_typeMonster_equipped_add_member是底层两步写法分别写入 union 的隐式类型字段与成员字段。union 字段在底层会附带一个隐藏的_type字段用于记录实际存储的类型这一点在教程的 union 序列化一节亦有说明见 docs/source/tutorial.md前两种写法正是自动处理了类型字段。为什么没有集成进flatc工具社区曾讨论过是否把 C 代码生成器并入flatc见 FlatCC 项目的 issue #1结论是三个候选方案都不具吸引力放弃独立 C 实现的 schema 编译器——不可接受导致大量代码重复——维护成本过高发明一套复杂的中间表示IR——代价过高。更何况无论是否集成FlatBuffers 的 C 运行时库都必须单独提供因此直接使用flatcc工具而非flatc并不会带来额外负担。这也是 C 语言绑定保持独立项目形态的根本原因。总结FlatCC 为纯 C 工程提供了与主项目flatc二进制兼容的完整方案flatcc编译器负责从.fbs生成 reader/builder/验证器代码flatcc_builder提供自底向上与自顶向下两种构建模型.bfbs反射支持运行时 schema 自省向量原地排序与既有 buffer 作为源则带来高效的缓冲区复用能力。在 C 项目中实践 FlatBuffers 时请记住用flatcc而非flatc生成代码二者布局兼容但工具不同create_as_root/buffer_create/start_as_root三套根构建方式不可混用用*_is_present判断字段显式存在用类型专用 add 方法简化 union 写入变更mutation能力有限不支持反射式修改与标量原地改写但支持向量排序与直接拷贝式的新缓冲区构建。进一步的入门流程可参考主仓库的 Tutorial选择 C 语言schema 语言细节见 Schema 文档示例 schema 位于 samples/monster.fbs构建与运行示例见 samples/sample_binary.cpp。【免费下载链接】flatbuffersFlatBuffers: Memory Efficient Serialization Library项目地址: https://gitcode.com/GitHub_Trending/fl/flatbuffers创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门