是的,FlatBuffers 完全支持 C 语言,并且为其提供了官方的、高质量的实现,C++ 版本是第一个实现,而 C 语言版本紧随其后,因为 FlatBuffer 最初就是为游戏开发(C++ 为主)和嵌入式系统(C 语言为主)设计的。

下面我将详细介绍 FlatBuffers 在 C 语言中的支持情况、如何使用以及它的优缺点。
为什么 FlatBuffers 适合 C 语言?
FlatBuffer 的设计哲学与 C 语言的需求非常契合:
- 高性能和零拷贝:这是 FlatBuffers 最大的优势,传统的序列化库(如 Protocol Buffers)需要先反序列化整个消息到一个内部结构体,然后再从该结构体复制数据到你的 C 结构体中,FlatBuffer 允许你直接读取底部的内存缓冲区,无需任何拷贝,这对于内存受限或性能要求极高的 C 语言应用(如嵌入式系统、游戏引擎)至关重要。
- 内存效率:FlatBuffer 数据是紧凑的二进制格式,没有额外的解析开销,反序列化过程就是简单的内存寻址和类型转换,非常轻量。
- 无需内存分配:读取 FlatBuffer 数据时,不需要在堆上进行任何内存分配,这对于嵌入式系统等难以管理动态内存的环境是一个巨大的优势。
- 无版本依赖:生成的 C 头文件不依赖于任何 FlatBuffer 的运行时库(除了一些基础类型和宏定义),这使得它非常适合集成到各种项目中。
如何在 C 语言项目中使用 FlatBuffers?
使用 FlatBuffers 主要分为两个步骤:Schema 定义与代码生成 和 序列化与反序列化。
定义 Schema 并生成 C 代码
-
安装 FlatBuffers 编译器: 你需要先安装 FlatBuffers 的编译器
flatc,你可以从 GitHub Releases 页面下载预编译的二进制文件,或者从源码编译。
(图片来源网络,侵删) -
创建
.fbsSchema 文件: 这是一种类似 IDL(接口定义语言)的文本格式,用来定义你的数据结构。monster.fbs示例:namespace my.game; enum Color:byte { Red = 0, Green = 1, Blue = 2 } struct Vec3 { x:float; y:float; z:float; } struct Monster { pos:Vec3; mana:short = 150; hp:short = 100; name:string; color:Color = Blue; inventory:[ubyte]; // A vector of unsigned bytes. // An enum that refers to the Color enum. // This demonstrates how enums can be used in different contexts. friendly:bool = false (deprecated); } -
使用
flatc生成 C 代码: 在命令行中运行以下命令:flatc --c monster.fbs
这会生成一个
monster_generated.h文件,这个头文件包含了所有必要的 C 结构体、枚举、访问函数和序列化/反序列化函数。
(图片来源网络,侵删)
在 C 代码中使用生成的代码
生成的 monster_generated.h 文件为你提供了所有工具,你不需要手动创建复杂的二进制数据,而是通过一个构建器来完成。
序列化示例 (serialize_monster.c):
#include <stdio.h>
#include <string.h>
#include "monster_generated.h" // 包含生成的头文件
int main() {
// 1. 初始化一个 FlatBufferBuilder,它是一个构建器,用于序列化数据。
// 参数是初始缓冲区大小,可以按需增长。
FlatBufferBuilder builder(1024);
// 2. 创建并嵌入数据,注意顺序:先创建依赖项(如 Vec3),再创建主对象(如 Monster)。
// 创建 Vec3
Vec3Builder pos_builder(builder);
pos_builder.add_x(1.0f);
pos_builder.add_y(2.0f);
pos_builder.add_z(3.0f);
auto pos = pos_builder.Finish();
// 创建 inventory 数组
unsigned char inventory[] = { 0, 1, 2, 3, 4 };
auto inventory_vector = builder.CreateVector(inventory, 5);
// 创建 name 字符串
auto name = builder.CreateString("MyMonster");
// 3. 创建 Monster 结构体
MonsterBuilder monster_builder(builder);
monster_builder.add_pos(pos);
monster_builder.add_name(name);
monster_builder.add_color(Color_Blue); // 使用 Color 枚举
monster_builder.add_inventory(inventory_vector);
auto monster = monster_builder.Finish();
// 4. 完成缓冲区
builder.Finish(monster);
// 5. 获取指向二进制缓冲区的指针和大小
uint8_t *buffer_ptr = builder.GetBufferPointer();
int size = builder.GetSize();
// buffer_ptr 包含了序列化后的 FlatBuffer 数据
// 你可以将其写入文件、发送到网络等...
printf("Serialized monster buffer size: %d\n", size);
// 写入文件
// FILE *f = fopen("monster.bin", "wb");
// fwrite(buffer_ptr, size, 1, f);
// fclose(f);
return 0;
}
反序列化/读取示例 (read_monster.c):
#include <stdio.h>
#include "monster_generated.h" // 包含生成的头文件
int main() {
// 假设我们从某个地方读取了二进制数据到 buffer 中
// 为了演示,我们复用上面序列化的逻辑
FlatBufferBuilder builder(1024);
// ... (序列化代码与上面相同) ...
Vec3Builder pos_builder(builder);
pos_builder.add_x(1.0f);
pos_builder.add_y(2.0f);
pos_builder.add_z(3.0f);
auto pos = pos_builder.Finish();
unsigned char inventory[] = { 0, 1, 2, 3, 4 };
auto inventory_vector = builder.CreateVector(inventory, 5);
auto name = builder.CreateString("MyMonster");
MonsterBuilder monster_builder(builder);
monster_builder.add_pos(pos);
monster_builder.add_name(name);
monster_builder.add_color(Color_Blue);
monster_builder.add_inventory(inventory_vector);
auto monster = monster_builder.Finish();
builder.Finish(monster);
uint8_t *buffer_ptr = builder.GetBufferPointer();
// --- 反序列化开始 ---
// 1. 验证缓冲区是否有效(可选,但推荐)
if (!MonsterBufferHasIdentifier(buffer_ptr)) {
printf("Invalid FlatBuffer file (wrong identifier)!\n");
return 1;
}
// 2. 获取根对象指针
auto monster_obj = GetMonster(buffer_ptr);
// 3. 直接读取数据,无需拷贝!
printf("Monster name: %s\n", monster_obj->name()->c_str());
printf("Monster HP: %d\n", monster_obj->hp());
printf("Monster position: x=%f, y=%f, z=%f\n",
monster_obj->pos()->x(),
monster_obj->pos()->y(),
monster_obj->pos()->z());
// 4. 读取数组
printf("Monster inventory: ");
auto inventory_vec = monster_obj->inventory();
for (int i = 0; i < inventory_vec->size(); i++) {
printf("%d ", inventory_vec->Get(i));
}
printf("\n");
return 0;
}
C 语言版本与 C++ 版本的对比
| 特性 | C 语言版本 | C++ 版本 |
|---|---|---|
| API 风格 | 函数式 API,monster_obj->name()->c_str()。-> 实际上是宏或函数调用,语法上模拟了指针访问。 |
面向对象 API,更像原生 C++,可以直接访问成员变量,monster_obj.name()->c_str()。 |
| 易用性 | 语法稍显冗长,需要记住函数名,但概念清晰。 | 更自然、更简洁,符合 C++ 程序员的习惯。 |
| 类型安全 | 编译时类型安全,但运行时通过 void* 传递,可能需要更多小心。 |
完全的类型安全,利用 C++ 的模板和重载。 |
| 性能 | 几乎相同,两者都实现了核心的零拷贝读取,C 版本在某些极致优化的场景下可能有一点点优势,但通常可以忽略不计。 | 几乎相同。 |
| 功能 | 功能齐全,支持所有核心特性。 | 功能最全,支持一些 C 版本没有的高级特性,如 std::string 字符串视图、反射等。 |
优缺点总结
优点:
- 极致性能:零拷贝读取,解析速度极快。
- 内存高效:二进制格式紧凑,读取时无需额外内存分配。
- 强前向/后向兼容性:Schema 的演进非常灵活。
- 无依赖:读取时无需任何运行时库。
- 跨平台:生成的是纯 C 代码,可轻松移植到任何有 C 编译器的平台。
缺点:
- 非随机访问:与 Protocol Buffers 不同,你不能通过字段名直接访问任意字段,FlatBuffer 是一个连续的字节流,读取必须从根开始,按顺序进行,虽然访问函数做了优化,但这是一种设计上的权衡。
- Schema 依赖:读取数据时,必须要有与数据版本完全匹配或兼容的 Schema(
.fbs文件)来生成头文件。 - C API 的学习曲线:对于习惯了 C++ 的开发者来说,C 语言的函数式 API 可能需要一点时间来适应。
FlatBuffers 对 C 语言的支持是官方、成熟且高性能的。 如果你正在开发一个对性能和内存有严苛要求的 C/C++ 项目,尤其是在嵌入式、游戏、高频交易或网络协议等领域,FlatBuffers 是一个非常优秀的选择,它的零拷贝特性和无依赖读取能力完美契合了 C 语言的设计哲学。
